I'm trying to create a plugin for an audio player where the user can specify their own optional html. The html that the user does specify should have certain properties that are defined in the player file.
At the moment in the plugin, the render method looks like this (cut for brevity example):
player.js:
render(){
return (
<div id="player">
<a class="jp-play" style={this.state.playStyle} onClick={this.play}>
<i class="fa fa-play"></i>
</a>
<div class="jp-current-time">{this.state.currentTime}</div>
</div>
);
Instead of hardcoding the JSX html in the plugin render (player.js) method I want to do something like this:
render(){
return (
{this.props.playerHtml}
);
where the parent calls this like so:
app.js:
render(
<Player playerHtml={getHtml()} />
, document.getElementById("app"));
function getHtml(){
<div id="player">
<a class="jp-play" style={this.state.playStyle} onClick={this.play}>
<i class="fa fa-play"></i>
</a>
<div class="jp-current-time">{this.state.currentTime}</div>
</div>
}
So the user can specify their own html for the plugin. This way instad of passing a fa-play icon within jp-play they can pass whatever they want.
The problem is that the onClick event handler and states passed through will no longer work properly as this.state.playStyle and this.state.currentTime will point to app.js file and not player.js.
My question is, how do I allow the user to supply their own html like this but bind the values of the html the user passes and the events to values and functions in my player.js? I could do this easily by modifying the dom but I don't think this is the react way.
You can pass the function to generate the player elements in a prop. The important thing is that it has to be binded to the Player component instance before being called.
Here there is a working version of your code. Take into account that you are returning a JSX element inside getHtml, not pure HTML, so be sure to change class for className and so on.
Important: I hope you understand the security implications of what you are trying to do.
class Player extends React.Component {
constructor(props) {
super();
this.state = {
playStyle: { background: 'red' },
currentTime: new Date()
}
}
render() {
return (
<div>
Player:
{ this.props.playerHtml.bind(this)() }
</div>
);
}
};
function getHtml() {
return (
<div id="player">
<a className="jp-play" style={this.state.playStyle} onClick={this.play}>
<i className="fa fa-play">Play</i>
</a>
<div className="jp-current-time">{this.state.currentTime.toString()}</div>
</div>
);
}
ReactDOM.render(
<Player playerHtml={getHtml} />
, document.getElementById("app")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
Related
I am getting a JSON response that has an image property for each key. This image property is the entire image element tag with properties. I am trying to pass that down into a child component, but upon attempting to render, it just shows up as plain html text on the page. Is there a way to render it as an actual image tag?
I have attached a screenshot of the JSON response
` <div className="aspect-[3/2] md:aspect-square overflow-hidden">
{props.image}
</div>
<span className="block py-5">{props.name}</span>`
You could use dangerouslySetInnerHTML but you'll have to wrap it in a span or div or something:
// Example class component
class Thingy extends React.Component {
render() {
const {source} = this.props;
console.log("rendered");
return (
<span dangerouslySetInnerHTML={{ __html: source }} />
);
}
}
// Render it
ReactDOM.render(
<Thingy source={`<img src="https://images.unsplash.com/photo-1490730141103-6cac27aaab94?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1000&q=80" />`} />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
The situation is a bit complicated:
inside a component called "LeftSectionHeader" I have a div, which when clicked must render a component;
the component to be rendered is called "ProfileMenu", and is basically a div that must be rendered on top of "LeftSectionHeader" itself and another div;
All these components are rendered inside another component called "Main".
The problem is that if I define the function inside "LeftSectionHeader", "ProfileMenu" will be rendered inside, while I need it to not only be rendered outside, but even cover it; that's why you'll see some boolean vars inside "Main", because that is the only way i could render it, but it still doesn't cover the other divs. I'll attach the code of each component and how the final result should look below.
LeftSctionHeader:
function LeftSectionHeader(){
return(
<div class="left-section-header">
<div class="crop" ><img src="./images/profiles/anonimous.png" /></div>
</div>
);
}
The div belonging to the "crop" class is the one that must be clicked to render "ProfileMenu"
ProfileMenu:
function ProfileMenu(){
return(
<div class="profile-side-menu">
//A lot of boring stuff
</div>
);
}
There are some functions related to this component, but they are not important, so I didn't put them, just ignore it
Main:
var p=true;
var m=true;
function Main(){
return(
<div class="main">
<Header />
<div class="left-section">
{m ? <div><LeftSectionHeader /><LangMenu /></div> : <ProfileMenu />}
</div>
{p ? <PostPage /> : <NoPostsMessage />} //Ignore this line
</div>
);
}
Before clicking on the orange div
After clicking
This might help as guidline, hopefully!
function LeftSectionHeader({ onClick }){
return(
<div class="left-section-header" onClick={onClick}>
<div class="crop" ><img src="./images/profiles/anonimous.png" /></div>
</div>
);
}
function Main(){
const [showProfile, setShowProfile] = useState(false);
return(
<div class="main">
<Header />
<div class="left-section">
{!showProfile ? (
<div>
<LeftSectionHeader onClick={() => setShowProfile(true)} />
<LangMenu />
</div>
) : <ProfileMenu />}
</div>
{p ? <PostPage /> : <NoPostsMessage />} //Ignore this line
</div>
);
}
The simplest solution might be to pass a handler into the header component to toggle the menu:
function App () {
const [showMenu, setShowMenu] = useState();
return (
<div>
<Header onMenuToggle={() => setShowMenu(!showMenu)} />
{ showMenu && <Menu /> }
</div>
)
}
function Header ({ onMenuToggle }) {
<div onClick={onMenuToggle}>...</div>
}
Caveat: This will cause the entire App component to re-render when the menu state changes. You can mitigate this by either
A) placing the menu state closer to where it's actually needed, like in the sidebar component instead of at the top, or
B) using a context or other orthogonal state store.
Another approach would be to leave the state handling in the LeftSectionHeader component and then use a React portal to render the menu elsewhere in the DOM.
I've created a component, and in that component is an audio tag. I've been trying to pass the file path of the mp3 to the component from its parent, but the audio element doesn't seem to be able to load the file successfully for some reason. The element just ends up greyed out, and I don't get any kind of error. I know the properties are being passed successfully because I'm also passing in the title of the track, and that loads just fine in the component. However, if I hardcode the same path in for the source, then it works fine. Which isn't a big deal, but I have 11 tracks to do and it would be much easier with a v-for statement.
Here's my parent component:
<template>
<div container>
<div class=banner>
<img src="../assets/CryptoLogo2.svg"/>
</div>
<div class="albumcontainer">
<div class="covercontainer"><img src="../assets/TIADCover(Final).png"/></div>
<div class="arrow"></div>
<div class="tracklistcontainer">
<table class="tracklist">
<tr class="track" v-for="track in tracks" :key="track.file" style="padding: 20px;">
<td>
<player :name="track.name" :file="track.file" />
</td>
</tr>
</table>
</div>
</div>
<div class="bio">
<p>
</p>
</div>
<div class="footer">
<div class="footericons">
<img src="../assets/icons/Facebook.svg" />
<img src="../assets/icons/Instagram.svg" />
<img src="../assets/icons/Twitter.svg" />
</div>
</div>
</div>
</template>
<script>
import player from './player'
export default {
name: "Album1",
data() {
return {
tracks: [
{
name: '5G',
file: '../assets/tracks/5G.mp3'
}
]
}
},
components:{
player
}
};
</script>
And then the component with the audio tag:
<template>
<div>
<h3>{{ name }}</h3>
<audio controls controlsList="nodownload">
<source ref="player" v-bind:src="file" type="audio/mpeg">
</audio>
</div>
</template>
<script>
export default{
name: "player",
props: {
name: {
type: String,
default: null
},
file:{
type: String,
default: null
}
}
}
</script>
Here you can see that my data is being passed successfully.
And you see that the source attribute is being loaded correctly as well.
This is what I get, and you can see the h3 loads fine, so I know the data is being passed. But the element is greyed out.
To test it out, I tried just hard coding the file path:
<source src="../assets/tracks/5G.mp3">
And that works just fine:
But I don't want to do it like that because I have about 11 tracks to do, so I would like for it to load from the data being passed so that I can reuse the component. Any ideas?
try using something like this in your Vue v-for I use this when src is not loading on the img tag.
:src="getSrc(x.src)"
methods: {
getSrc(src) {
return require("../assets/" + src);
}
}
Your watch function on file prop is never triggered because your file name is static, it never triggers a change on the watcher, before mounting the component, at least in the code you provided it is that way.
From Vue.js API vm.$watch
Watch an expression or a computed function on the Vue instance for changes. The callback gets called with the new value and the old value.
I'm trying to display a div when the mouse is over another div element. I've managed to do so via onMouseEnter and onMouseLeave.
The issue here is that if you quickly move from one div to another (it's an array of divs that contain data about a product), the value of index[0] becomes true.
The way it works is that I have an array initialised to false when the mouse enters one of them, it becomes true and shows the div that I wanted. Once it leaves, it set it back to false.
this.state = {
isProductsHovering: new Array(this.props.currentProducts.length).fill(false)
};
handleMouseHover = (idx) => {
this.setState({
isProductsHovering: update(this.state.isProductsHovering, {
[idx]: { $set: !this.state.isProductsHovering[idx] }
})
})
}
render() {
return this.props.currentProducts.map((product, idx) => {
return <Fragment key={idx}>
<div className="product-grid-view col-6 col-md-4" >
<div
className=" product-holder"
onMouseEnter={this.handleMouseHover.bind(this, idx)}
onMouseLeave={this.handleMouseHover.bind(this, idx)}>
<div className="image-container" align="center">
<img src={"/img/product-3.jpg"} alt="" />
{
this.state.isProductsHovering[idx] &&
<div className="product-buttons">
<Link to={`products/${product.id}`} className="btn-detail" text="View Details" />
<Link to='#' className="btn-cart" icons={["icon-cart", "icon-plus"]} />
</div>
}
</div>
<div className="details-holder">
<span className="part-text">{product.desc}</span><br />
<span className="manufacturer-text">{product.manufacturer.name}</span>
<div className="product-review_slide">
<Stars values={product.averageRating} {...starsRating} />
<span className="product-review">{getLength(product.reviews)} review</span>
</div>
<span className="product-price">{product.salesPrice.toFixed(2)}</span>
<span className="product-currency">SR</span>
</div>
</div>
</div>
</Fragment>
})
}
Update
I've made a stackblitz project to reproduce the same issue as suggested:
https://stackblitz.com/edit/react-mouse-hover.
For everyone that wants to see what I mean. I've attached a photo of the issue. If you move the mouse over the two divs (up and down as quick as you can), this what happens:
mouse hover broken
For situation like this, I wouldn't rely on array and index to make it work. You are further complicating your handleMouseHover functions and the checking of isHovering.
A 'more React' way of dealing with this situation is simply make each Product a component itself. And this Product component will have its own state of isHovered and handleOnHover method, that way you create a more concise and reliable code without having to rely on array index at all:
App.js can be as simple as this:
class App extends Component {
render() {
return (
<div>
{
data.map(product =>
<Product product={product} />
)
}
</div>
)
}
}
A new Product.js:
import React from 'react'
import ReactHoverObserver from 'react-hover-observer';
export default class Product extends React.Component {
render() {
const { product } = this.props
return (
<ReactHoverObserver className="product-grid-view col-6 col-md-4">
{
({isHovering}) => (
<div className=" product-holder">
<div className="image-container" align="center">
<img src={"/img/product-3.jpg"} alt="" />
{
isHovering &&
<div className="product-buttons">
<button className="btn-detail">View Details</button>
</div>
}
</div>
<div className="details-holder">
<span className="part-text">{product.desc}</span><br />
<span className="manufacturer-text">{product.manufacturer.name}</span>
<div className="product-review_slide">
<span className="product-review">0 review</span>
</div>
<span className="product-price">{product.salesPrice.toFixed(2)}</span>
<span className="product-currency">Currency</span>
</div>
</div>
)
}
</ReactHoverObserver>
)
}
}
I have put the moficiation in Stackblitz: https://stackblitz.com/edit/react-mouse-hover-2cad4n
Liren's answer is good advice and will help simplify the code. One thing I also noticed is that occasionally the HoverObserver won't 'hear' an event, and since the hover enter and hover exit events are listening to the same event, then the display state for the button will become reversed (i.e., it will show when the mouse is NOT hovering and hide when the mouse hovers over the observer).
I would recommend getting rid of the ReactHoverObserver HOC and instead just listen for the onMouseOver for hover enter and onMouseLeave for hover exit. That way, even if the div doesn't register a hover enter or exit, it will easily reset because onMouseOver will toggle the display state to true and onMouseLeave will reliably set the button's display state to false.
See here for those events in the docs:
https://reactjs.org/docs/events.html#mouse-events
The way you trigger it (from array or from a component) is semantics , the real issue is that these events don't always fire.
I had the same issue , apparently the events of react are not that reliable.
In my case I could live in a situation where a tooltip does not close , but not in the situation where 2 tooltips are open. In order to solve my issue , I returned to good old dom manipulation - Every time a tooltip appeared , it made all the other ones invisible.
It looked something like this :
showTooltip() {
// Clear old tooltips, EventTooltip is the name of my tooltip class.
Array.from(document.getElementsByClassName('EventTooltip'))
.forEach(tooltip=>tooltip.style = 'display:none;')
this.setState({showTooltip: true})
}
hideTooltip() {
this.setState({showTooltip: false})
}
I have been going around the internet for a while with no hope at all.
I am using "collapse" bootstrap for toggling visibility of a div.
I need to control this toggling through react onClick. Bootstrap says can be accessed through "$('.collapse').collapse()" but for some reason i canot do this in react.....any suggestions other than importing jQuery or using react-bootstrap component ??
class Container extends Component {
render() {
return (
<div className="wrapper">
<div className="collapse in" id="collapseExample">
<h1>Hide me</h1>
</div>
<div className="container">
<div className="row">
<button
data-toggle="collapse"
data-target="#collapseExample"
aria-expanded="false"
aria-controls="collapseExample">
Get a random Book
</button>
</div>
</div>
</div>
)
}
Bootstrap javascript depends on jQuery. jQuery and React have different ways to manipulate the DOM. To avoid issues in the future, you shouldn't use both. If you are using React, thinking in React is important.
A good React wrapper of Bootstrap is reactstrap. I recommend you use the library to build Bootstrap-based component, not write the Bootstrap raw class. A component named Collapse might fit your need, as you can see in sample code here.
If you are not interested in importing any library at all, it's fairly simple to write the component by yourself, using state. The internal idea is similar to the Collapse sample code above:
class MyCollapse extends Component {
state = {
isOpen: false
};
toggleState = () => this.setState( prevState => ({ isOpen: !prevState.isOpen }));
render() {
return (
<div>
<button onClick={this.toggleState}>Click to toggle</button>
{
this.state.isOpen &&
<div className="my-collapse">
{/* COLLAPSE CONTENT */}
</div>
}
</div>
)
};
}
Every time you click the button, the state attribute isOpen is toggled, which leads to a re-render of the component. That's one of the biggest strength of React: very flexible.
The following snippet illustrates how it should be done. If its still not working correctly in your program i would verify the CDN script links are correct.
Reminder, in ReactJS we should never use jQuery we manipulate the virtual DOM instead via components.
// Example stateless functional component
const SFC = props => (
<div>{props.label}</div>
);
// Example class component
class Container extends React.Component {
render() {
return (
<div className="wrapper">
<div className="collapse" id="collapseExample">
<h1 className="card card-body">Hide me</h1>
</div>
<div className="container">
<div className="row">
<button className="btn btn-primary" type="button" data-toggle="collapse" data-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">Get a random book</button>
</div>
</div>
</div>
) }
}
// Render it
ReactDOM.render(
<Container/>,
document.getElementById("react")
);
<div id="react"></div>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/js/bootstrap.min.js" integrity="sha384-a5N7Y/aK3qNeh15eJKGWxsqtnX/wWdSZSKp+81YjTmS15nvnvxKHuzaWwXHDli+4" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>