Accessing and modyfing slot's elements in Vue 3 - javascript

I'm rewriting my MQTT based dashboard from Vue 2 to Vue 3 currently, and can't solve one problem.
The dashboard has many Vue components, which reacts to specific MQTT topics and values, to display current system state. One of them is the mqtt-multi-state component which is declared like below:
// status-page
<mqtt-multi-state subscribe-topic="home/env/sensor/wc/door/status" json-path="state">
<div value="OPEN"><font-awesome-icon icon="door-open"/></div>
<div value="CLOSED"><font-awesome-icon icon="door-closed"/></div>
</mqtt-multi-state>
It contains dwo div elements, with two states that the system sensor (door) can has. These elements are passed to the default slot and are hidden by default via css.
What I want to achieve is to show one of them based on the equality of the value attr in each of them with the current MQTT value. So if the current value is OPEN then the first div show up, when value is CLOSED then the second one appears.
// mqtt-multi-state
<template>
<div class="mqtt-multi-state">
<div>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
methods: {
messageArrived(value){
let states = this.$slots.default() // here are the two divs
for(let i = 0;i < states.length;i++ ){
if(states[i].props.value === value )
{
//states[i].elm.className = "state-active" <- Vue 2 solution using DOM with elm
//states[i].props.class = "state-active"; <- Vue 3, doesn't work, not reactive?
}
else
{
//states[i].props.class = "";
}
}
}
}
}
</script>
I know, this approach is a little bit different but I really like to describe dashboard element in this way in HTML. In the messsageArrive() method I'm iterating over the default slot children elements and if the value match the current value prop I want to show this item, by add a state-active class. But this solution does not work. The VNode is changed (checked in the console).
In Vue 2 I've simply get to the DOM element directly and change it class, but on Vue 3 I can't figure it out, how to get from VNode to the DOM Node. Or there are maybe an other/better way to do that?

Well, many evenings later I came to this solution. Instead of putting all the different states into a default slot, I've created dynamic named slots.
The different state elements (font-awesome icons in this case) go to each slot's template element. Unfortunately I can't pass the mqtt value to the template element itself, because it does not exists inside mqtt-multi-state parent component. Instead, I've passed the mqtt value into the template's first child element. It can be any element, div, p, font-awesome-icon.
// status-page
<mqtt-multi-state :state-count="2" subscribe-topic="home/env/sensor/wc/door/status" json-path="state">
<template #0><font-awesome-icon value="OPEN" icon="door-open"/></template>
<template #1><font-awesome-icon value="CLOSED" icon="door-closed"/></template>
</mqtt-multi-state>
There is also a prop state-count that defines the number of different states. Note the colon before prop name, so the string "2" is a Number type.
The mqtt-multi-state component becomes like this:
// mqtt-multi-state
<template>
<div class="mqtt-multi-state">
<template v-for="(slot, index) in slots">
<slot :name="index" v-if="slot" ></slot>
</template>
</div>
</template>
<script>
export default {
data: function(){
return {
slots: []
}
},
props: {
stateCount: {
default: 2,
type: Number
}
},
methods: {
messageArrived(value){
for(let i = 0; i < this.stateCount; i++ ) {
this.slots[i] = this.$slots[i]?.()[0]?.props?.value === value
}
}
}
}
</script>
In the messageArrived() function the code iterates through all the slots and compares the slot's first child element prop, named value with the current mqtt value received. If the values are equal then the this.slots[i] array value goes true, and the Vue's v-if directive makes corresponding slot visible, otherwise hides the slot.
Named slots names are created by the index value, so to get their content with this.$slot.slotNameRenderFunction() I call the function 0(), 1() etc using the this.$slot[index]() notation (accessing object member by name stored in the variable) with an optional chaining operator ?. - so the whole notation looks a little weird :).

Related

Vue v-model targeting other elements within the v-for loop -- how to construct the v-model array structure to avoid this

Summary: My v-model input affects other elements in the same array but I expect it to just target the input I am amending.
In my Vue data store I have an array userAnswers of 2 objects (0 and 1). This is constructed on created created() (mentioning in case this is relevant).
You can see an the structure of the data here:
userAnswers:Array[2]
0:Object
answers:Object
1:"3"
10:"3"
13:"3"
15:"99"
18:"3"
id:4
1:Object
answers:Object
1:"3"
10:"3"
13:"3"
15:"99"
18:"3"
id:3
In this example I have question IDs 1,10,13 etc. and I have used the following v-model wrapped in a v-for loop to produce two inputs.
<div v-for="(group, key) in selectedAssessment.questions" :key="key">
<div
v-for="(participant, i) in participants"
v-bind:key="participant.id"
>
<input type="text" v-model="form['userAnswers'][1]['answers'][question.id]" />
</div>
</div>
This loops over the questions and then within that loop creates an input for each user. When I change the input for a user it is bound to both inputs within the question loop when I am expecting the v-model just to target the specific user question id.
Is there something I missing with my v-model="" reference that causes this?
Screenshot included for some clarity.
UPDATE:
As per recommendation I have created a jsfiddle and isolated the issue. https://jsfiddle.net/raisonon/jf5t460s/44/
I am constructing the data object on created() and this causes the issue. I'm not sure why though.
created() {
// Form Answers -- Add empty answers arr
this.form.userAnswers = this.form.userAnswers.map((el) => {
var o = Object.assign({}, el);
o.answers = this.questionIds;
return o;
}, this.questionIds);
}

How to render multiple HTML parts with a plain Javascript function

This is a static website with hundreds of pages. I need to render elements like a topnav or a newsletter or a strap of content and changing those contents periodically, from JS.
This is what I tried:
const components = {
compartirEnFlex: `<h4>Newsletter</h4>`,
newsletterEs: `<h4>Compartir</h4>`,
}
const ids = ['newsletterEs', 'compartirEnFlex', 'infoArticulo', 'infoDeLaWebEnFlexIzq']
function renderComponents(objWithComp, idsArr){
return idsArr.map(function(id){
for(let component in objWithComp){
let arrOfIds = Object.keys(objWithComp);
arrOfIds.map(key => key)
if(id === key){
document.getElementById(id).insertAdjacentHTML('afterbegin', objWithComp[id])
}
}
})
}
renderComponents(components, ids);
Each id has its counterpart in the HTML structure. When I do this individually it works. However, I have to handle this in an elegant way (and there is no possibility for a JS framework like React in this project).
Thanks for the help!
When you run your code, you'll see the error Uncaught ReferenceError: key is not defined in the console.
That's because key in if(id === key) is not defined. The line arrOfIds.map(key => key) returns the same exact array as arrOfIds because Array.prototype.map "returns a new array populated with the results of calling a provided function on every element in the calling array."
Here, you don't assign that new array to a variable, so nothing happens. Even if it was, that new array would be a copy of arrOfIds because your mapping function (key) => key returns key for every key -- meaning that the output is the same as the input.
However, that's not an issue here. If I understand your question correctly, then this demo should show an example of what you're trying to accomplish. If that's what you want to achieve, then here's a solution:
First, you don't need to iterate for component in objWithComponent inside idArr -- you're already doing that in the idArr. You don't need the ids array either, because you can get the keys of the components from the components object using Object.keys().
Let's say your HTML looks something like this:
<div>
<div id="newsletterEs"></div>
<div id="compartirEnFlex"></div>
<div id="infoArticulo"></div>
<div id="infoDeLaWebEnFlexIzq"></div>
</div>
Then, using Object.keys(components) to get an array of the ids of the components that you have, you can map those to HTML tags. In fact, map is not necessary here because map returns a new array, and unless you need that array later, there's no reason to use map. Instead, you can use Object.prototype.forEach.
Here's what that would look like:
const components = {
compartirEnFlex: `<h4>Newsletter</h4>`,
newsletterEs: `<h4>Compartir</h4>`,
}
function renderComponents(objWithComp) {
Object
.keys(components)
.forEach((id) => {
const element = document.getElementById(id)
const component = objWithComp[id]
if (component && element) {
element.insertAdjacentHTML('afterbegin', component)
}
})
}
renderComponents(components)
Then, when you call renderComponents, you can pass just the components argument, and only render the components for which divs with corresponding ids exist with an if statement.

Reference element property within another property

I am trying to access properties (fieldType, value) within an dynamic property (:class)
<div fieldType="favoriteSports"
#click="update_favorite_sports"
value="Soccer"
:class="{selected: sportsForm[fieldType].indexOf(value) != -1 }">
Soccer
</div>
This logs the following error:
Property or method "fieldType" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I suppose it is expecting fieldType to be a data property.
How can this be achieved, or is this an anti-pattern?
Using a v-for to fill values exposes the entire object to the template syntax
I'm assuming your object system but it's universal to different structures.
<div
v-for="sport in sports"
:fieldType="sport.type"
#click="update_favorite_sports(sport)"
:value="sport.name"
:class="{selected: sportsForm[sport.type].indexOf(sport.name) != -1 }"
>
Soccer
</div>
Though honestly you no longer need the value and fieldType properties unless you're referencing them somewhere.
That will allow you to use
methods: {
update_favorite_sports (sport) {
// Whatever your code is guessing something like
let index = this.sportsForm[sport.type].indexOf(sport.name)
if(index >= 0) {
this.sportsForm[sport.type].splice(index, 1)
return
}
this.sportsForm[sport.type].push(sport.name)
}
}
So that the div acts like a radio select.

Vue.js - repeat an element a specific number of times

I would like to use Vue.js to repeat an element n times, based on the num-labels prop.
<label-parent num-labels="4"></label-parent>
This is my component:
<template>
<div id="label-parent">
<div v-for="idx in numLabels">
{{idx}}
</div>
</div>
</template>
<script>;
export default {
name: "LabelParent",
props: ['numLabels'],
}
</script>
This code only outputs an empty div: <div id="label-parent"></div>. What is the correct way of doing this?
v-for can also take an integer. In this case it will repeat the template that many times.
<div v-for="n in 4">{{n}}</div>
and you will need to use v-bind:num-labels="4" to parse the value as number.
Vue Docs
<label-parent num-labels="4"></label-parent>
Passes 4 as a string which won't work with v-for.
Pass your argument with : to evaluate the expression so you get an actual number which will work with v-for.
<label-parent :num-labels="4"></label-parent>
BTW:
I highly suggest typing your props.
You can type your props like this:
export default {
props: {
numLabels: {
type: Number, // type of the property
required: (true|false), // is this prop required or not?
default: 0 // default value for this prop
}
}
}
See Prop Validation
As some people notice, this is because you're passing a 4 as string not integer
This should work
<label-parent :num-labels="4"></label-parent>
And this is the docs reference
Components Literal-vs-Dynamic

Check if variable is React node or array

I'd like to have a condition that states if a prop is a React node then just place it as a child within a component, and if it's not, take some action to make it into a component.
This way my component will be able to accept this prop as an array of strings, or an array of nodes.
I tried to check if React.PropTypes.node would return a boolean but it doesn't work.
Say I have a module called List and there's a prop called items. I'd like to be able to pass
var items = [
"One",
"Two",
"Three"
]
as well as
var items = function () {
return (
<li>One</li>
<li>Two</li>
<li>Three</li>
)
}
And within the component have some logic that would detect the difference and if it's a plain array (not an array of nodes) be able to map the items.
React has a function just to check if a variable is an element, here's the docs.
React.isValidElement(obj)

Categories

Resources