Check if variable is React node or array - javascript

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)

Related

Accessing and modyfing slot's elements in Vue 3

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 :).

Render object in React

I want to render Object "genres" but my console returns to me :
Error: Objects are not valid as a React child (found: object with keys {genre}).
If you meant to render a collection of children, use an array instead.
My code
function Movie({title,summary,year,poster,genres}){
return(
<div className="movie">
{genres.map(genre=>{return {genre}})} //doesn't work
{genres.map(genre=>{return <li>{genre}</li>})}
</div>
)
}
the first code
{genres.map(genre=>{returns {genre}})}
doesn't work
But the second code below works well.
{genres.map(genre=>{returns <li>{genre}</li>})}
What's the difference between these two things?
This line
{genres.map(genre=>{returns {genre}})}
returns an object, out of each item in the array: { genre: 'drama' }
However, this other line
{genres.map(genre=>{returns <li>{genre}</li>})}
The { inside the JSX for li won't turn it into an Object, but will access its value: <li>drama</li>
The first code returns an object with key "genre" which its value is the genere value, and react can't render it.
The second one return the value of genre, because the braces here aren't an object braces but the correct syntax of putting variables inside html in react.
If you want to return the value without li, do: {genres.map(genre=> genre)}

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.

Confusion with map() in javascript

I was learning React and wanted to use map() function. Here i the code:
class App extends Component {
state = { car:[
{carTitle:'Toyota'},
{carTitle: 'Honda'},
{cartitle: 'Chevrolet'}
] };
render() {
return ( <div>
{this.state.car.map((item, index)=>
<h1>{item.carTitle}</h1>
)}
</div> );
}
}
The question is why if I use
{this.state.car.map((item, index)=>
<h1>{item[index].carTitle}</h1>
I get an error. Since I have array of objects, I think it is logical to use {item[index].carTitle}. But if I use <h1>{item.carTitle}</h1>, all works ok. Why? Did I misunderstood something?
When you use .map() you need to pass a function as a callback, like the one you are already passing: .map( (item, index) => ... )
The first argument of this callback function, item, is the current element that is been processed in the array. index is the current position.
item already returns the content of each position of the array, in this case each object that you defined in car.
So, your second example, item[index].carTitle, is incorrect because you are trying to access an array position that doesn't exist inside each object.
Also, everytime you use a .map() in ReactJS applications, you need to specify a key to the element you are returning inside this function, in this case: <h1 key={index}>{{car.carTitle}}</h1>.
Golden tip: to make your code clearer and avoid confusion you should rename car to cars, as it is a list of cars, and instead of using item you should use car. It will make your code more legible and anybody that reads your code can understand it easier than if you are using generic names. So it would look something like this:
cars: [
{carTitle:'Toyota'},
{carTitle: 'Honda'},
{cartitle: 'Chevrolet'}
]
and:
{this.state.cars.map( (car, index) => {
<h1 hey={index}>{{car.carTitle}}</h1>
})}
Further readings:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
https://reactjs.org/docs/lists-and-keys.html
Clean Code, Book by Robert Cecil Martin

React.js onClick in recursive function

I'm doing a hierarchy tree view in React.js by recursion. I fetch categories from the DB and prepare an array in a structure that each array element is a object that has a prroperty that is an array consisting of his children:
React.js call recursive function in render
I'm already able to generate the view, but now my problem is setting the onClick={
} property for each element.
For some reason, the elements of the first level of the tree works well with the onClick function, but starting at level two, whenever I click any element, the function that is bound to the click is called two, three, four times, dependends on the current element's tree level.
MY function with the onClick:
printTree: function(level){
var self = this;
var level_length = level.length;
if(level_length >= 1){
return(
<ul>
{
level.map(function(category){
return (
<li key={category.fields.name} onClick={self.editCategory.bind(self,category)}><a>{category.fields.name}</a>
{ self.printTree(category.sons_array)}
</li>
)
})
}
</ul>
)
}
}
The input parameter level is always an array..that consists of a level of the tree,a collection of objects that will be processed this iteration.
I think that my problem is calling the function recursively passing self. In the first time that the function is called, self points to the actual this and everything works fine, but starting ata level two in the recursion, self will be pointing to the previous function in the recursive call and not to the this, but I don't know how to fix it.

Categories

Resources