I am using the mermaid library to build flowcharts. The principle of its work is that inside a block there is a pseudocode - commands of a special syntax, on the basis of which the flowchart is built in the block.
I want to be able to change the contents of the block dynamically, and the script rebuilds the block diagram every time.
How should I set up initialization? Perhaps I should add some callback function in the settings?
I initialized in this way:
mermaid.init({/*what setting parameters should be here?*/}, ".someClass"/*selector*/);
but the script doesn’t render any new commands. It only renders the commands that existed at the moment the document was loaded.
In other words, I want to edit a flowchart online.
function edit() {
const new_mermaid = document.createElement("div");
new_mermaid.classList.add("mermaid");
new_mermaid.classList.add(".someClass");
/*new_mermaid.innerHTML =
`graph TD
1[point 1] --> 2[point 2]`;*/
// it doesn't work when I append the new element dynamically!
new_mermaid.innerHTML = document.querySelector(".mermaid").innerHTML;
// it works always.
document.body.append(new_mermaid);
/* document.querySelector(".mermaid").innerHTML =
`
graph TD
A --> B`*/
// it doesn’t work with event listener
}
edit(); // it works
document.body.addEventListener("click", edit)
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>
// how to do it correctly?
mermaid.init({
noteMargin: 10
}, ".someClass");
</script>
<div class="mermaid someClass">
graph TD
1--> 2
3 --> 2
2 --> 1
</div>
It seems, I know the answer. Look at the solution below:
document.querySelector("button").addEventListener("click", (e) => {
const output = document.querySelector(".flowchart");
if (output.firstChild !== null) {
output.innerHTML = "";
}
const code = document.querySelector(" textarea").value.trim();
let insert = function (code) {
output.innerHTML = code;
};
mermaid.render("preparedScheme", code, insert);
});
<script src="https://unpkg.com/mermaid#7.1.0/dist/mermaid.min.js"></script>
<p>Input your data:</p>
<div class="input">
<textarea style="width:300px; height:200px"></textarea>
<br>
<button>render</button>
</div>
<div>
<p>output:</p>
<div class="render_container" style = "width:300px; height:200px; border:thin solid silver" >
<div class="flowchart"></div>
</div>
</div>
Thanks for the answer above. I would like to add a react wrapper to the answer scope for whoever using react:
import React, {Component} from "react";
import mermaid from "mermaid";
export default class Mermaid extends Component {
constructor(props){
super(props)
this.state={
chart: this.props.chart || ""
}
mermaid.initialize({
mermaid : {
startOnLoad: false,
}
})
this.mermaidRef = React.createRef()
}
mermaidUpdate(){
var cb = function (svgGraph) {
this.mermaidRef.current.innerHTML = svgGraph
};
//console.log("this.state.chart", this.state.chart)
mermaid.mermaidAPI.render('id0', this.state.chart, cb.bind(this));
}
componentDidMount(){
this.mermaidUpdate()
}
componentDidUpdate(prevProps, prevState) {
//console.log("Mermiad prevProps.chart", prevProps.chart)
if (this.props.chart !== prevProps.chart) {
this.setState({chart:this.props.chart},()=>{
this.mermaidUpdate()
})
}
}
render() {
var outObj = (
<div
ref={this.mermaidRef}
className="mermaid"
>
{this.state.chart}
</div>
)
return outObj
}
}
Related
Is it possible to execute a <Script/> every time the props of a react/nextjs component change?
I am converting markdown files to html using marked and, before rendering the html, I would like to have a [copy] button on each <pre> block (those are the code blocks). I have a <script/> that iterates through the <pre> blocks of the DOM document.querySelectorAll("pre") and injects the button needed. If the html changes though at a later stage, then I have found no way to re-run the script to add the copy buttons again.
I have the impression that this is not a very react/nextjs way of doing this, so any hints would be appreciated.
The Script to add the copy buttons. I have added this as the last tag of my <body>:
<Script id="copy-button">
{`
let blocks = document.querySelectorAll("pre");
blocks.forEach((block) => {
if (navigator.clipboard) {
let button = document.createElement("img");
button.src = "/images/ic_copy.svg"
button.title = "Copy"
button.id = "copy"
button.addEventListener("click", copyCode);
block.appendChild(button);
}
});
async function copyCode(event) {
const button = event.srcElement;
const pre = button.parentElement;
let code = pre.querySelector("code");
let text = code.innerText;
await navigator.clipboard.writeText(text);
button.src = "/images/ic_done.svg"
setTimeout(()=> {
button.src = "/images/ic_copy.svg"
},1000)
}
`}
</Script>
the React component. Not much to say here. The content is coming from the backend. Not sure what would be the 'React' way to do this without the script.
export default function Contents({ content }) {
return (
<div className='pl-2 pr-2 m-auto w-full lg:w-2/3 mb-40 overflow-auto break-words'>
<div className="contents" dangerouslySetInnerHTML={{ __html: content }} />
</div>
)
}
You should absolutely not do this and instead incorporate this logic into your react app, but if you must you can leverage custom window events to make logic from your html script tags happen from react.
Here is an example script:
<script>
function addEvent() {
function runLogic() {
console.log("Stuff done from react");
}
window.addEventListener("runscript", runLogic);
}
addEvent();
</script>
And calling it form react like this:
export default function App() {
const handleClick = () => {
window.dispatchEvent(new Event("runscript"));
};
return (
<div className="App" onClick={handleClick}>
<h1>Hello</h1>
</div>
);
}
i am trying to implement an ui requirement. I want to add a active class name to the children div one at a time. 1st it will add the class in first child, and then the class will be removed and to be added in the 2nd child div. And it will infinitly itereate.
Here is my code in next js
$(".softwares_container").each(function () {
(function ($set) {
setInterval(function () {
var $cur = $set
.find(`.${st.active}`)
.removeClass(`${st.active}`);
//store inner html of current item
var $next = $cur.next().length
? $cur.next()
: $set.children().eq(0);
$next.addClass(`${st.active}`);
//store inner element of next item
//set inner html of current item to inner html of next item
var $next_inner = $next.children().eq(0);
setValue({
name: $next_inner.attr('alt'),
description: $next_inner.attr('data-info')
})
// setImage($next_inner.attr('src'))
}, 1000);
})($(this));
});
<div className={`softwares_container ${st.left_container}`}>
<div className={` ${st.img}`} alt="1">
<img src={ae.src} data-info="this is aftereffects" alt="After effects" />
</div>
<div className={st.img} alt="2">
<img src={pr.src} alt="Adobe Premiere pro" />
</div>
<div className={st.img}>
<img src={ps.src} alt="Adobe Photoshop" />
</div>
<div className={st.img}>
<img src={xd.src} alt="Adobe Xd" />
</div>
</div>
But it is not working.it is showing unexpected behaviour. It works fine in react .
Can anyone please give me an alternative solution or tell me how to fix the issue?
Here's the link where you can see the unexpected behaviour.
https://diptnc.ml/about
You can write an effect that sets the classname for elements in an array in a round-robin manner.
// Keep the interval id around so that
// it can be cleared when unsubscribing the effect.
let activeFxId;
/*
Applies active class to an array of HTMLElement in a round-robin manner.
*/
function activeFx(elements) {
activeFxId = setInterval(() => {
const elementsArr = [...elements];
let idx = elementsArr.findIndex((element) => {
return element.classList.contains('active');
});
if (idx === -1) {
idx = 0;
}
elementsArr[idx].classList.remove('active');
elementsArr[(idx + 1) % elementsArr.length].classList.add('active');
}, 2000);
return () => {
clearInterval(activeFxId);
};
}
How you provide this array of elements is left to you. An approach is to store a ref to the parent element containing them and pass that on to the function.
For example,
/* Example component */
import React, {useEffect, useRef} from 'react';
export default () => {
const ref = useRef();
useEffect(() => {
if (ref.current && ref.current.children) {
return activeFx(ref.current.children);
}
});
return (
<div ref={ref}>
<div>One</div>
<div>Two</div>
<div>Three</div>
</div>
);
};
I'm trying to use the sensor's API with react and I can't seen to be able to make it work.
It gives me an error saying that AmbientLightSensor (in my case this sensor) is undefined
If I run the script outside react ( more exactly with an extension from VSCode "live server" ) it works fine ( just a html with some JS code in it ).
That's cool and all but in this case at least I want to run this script inside react and it just doesn't let me.
So far I've tried:
running this code in react as a class method called by componentDidMount and that (Simply i've put my JS code in there ^^ )
running this code with the tag hoping that maybe react isn't really using the JS that I know and that maybe running it inside html will change this ... No it didn't do the trick
So at this point I'm unsure what to even check to make this work
Here is my code, the js code I'm trying to run is inside the Did mount component
class App extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const details = document.getElementById("details");
// Feature detection
if (window.AmbientLightSensor) {
try {
const sensor = new AmbientLightSensor();
// Detect changes in the light
sensor.onreading = () => {
details.innerHTML = sensor.illuminance;
// Read the light levels in lux
// < 50 is dark room
if (sensor.illuminance < 50) {
document.body.className = "darkLight";
} else {
document.body.className = "brightLight";
}
};
// Has an error occured?
sensor.onerror = event =>
(document.getElementById("details").innerHTML =
event.error.message);
sensor.start();
} catch (err) {
details.innerHTML = err.message;
}
} else {
details.innerHTML =
"It looks like your browser doesnt support this feature";
}
}
render() {
return (
<div className="app">
<h1>Ambient Light Sensor</h1>
<p>Current Light Levels</p>
<div id="details"></div>
</div>
);
}
}
And also here is the working html
<head>
<meta charset="UTF-8" />
<title>Ambient Light Sensor</title>
</head>
<body class="brightLight">
<h1>Ambient Light Sensor</h1>
<p>Current Light Levels</p>
<div id="details"></div>
</body>
<script>
const details = document.getElementById("details");
// Feature detection
if (window.AmbientLightSensor) {
try {
const sensor = new AmbientLightSensor();
// Detect changes in the light
sensor.onreading = () => {
details.innerHTML = sensor.illuminance;
// Read the light levels in lux
// < 50 is dark room
if (sensor.illuminance < 50) {
document.body.className = "darkLight";
} else {
document.body.className = "brightLight";
}
};
// Has an error occured?
sensor.onerror = event =>
(document.getElementById("details").innerHTML =
event.error.message);
sensor.start();
} catch (err) {
details.innerHTML = err.message;
}
} else {
details.innerHTML =
"It looks like your browser doesnt support this feature";
}
</script>
</html>```
PS* for this to work you need to run this on a https server
That's not how React works...
I'd suggest looking into dangerouslySetInnerHTML link here: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
And createRef link here: https://reactjs.org/docs/refs-and-the-dom.html#creating-refs
Here is a simple example utilizing both to give you a better idea:
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.detailsRef = React.createRef();
}
createMarkup() {
return { __html: 'whatever you want...' };
}
componentDidMount() {
console.log(this.detailsRef.current.innerHTML);
}
render() {
return (
<div className="app">
<h1>Ambient Light Sensor</h1>
<p>Current Light Levels</p>
<div
ref={this.detailsRef}
dangerouslySetInnerHTML={this.createMarkup()}
/>
</div>
);
}
}
export default App;
Play around with it and read the links from the official docs to adapt to your specific use case...
Never used the AmbientLightSensorAPI before but: try something like this:
class App extends Component {
constructor(props) {
super(props);
this.state = {
details: ''
};
}
componentDidMount() {
if (window.AmbientLightSensor) {
try {
const sensor = new AmbientLightSensor();
sensor.onreading = () => {
this.setState({ details: sensor.illuminance });
if (sensor.illuminance < 50) {
document.body.className = 'darkLight';
} else {
document.body.className = 'brightLight';
}
};
sensor.onerror = event =>
this.setState({ details: event.error.message });
sensor.start();
} catch (err) {
this.setState({ details: err.message });
}
} else {
this.setState({
details:
'It looks like your browser doesnt support this feature'
});
}
}
render() {
return (
<div className="app">
<h1>Ambient Light Sensor</h1>
<p>Current Light Levels</p>
<div id="details">{this.state.details}</div>
</div>
);
}
}
Ok so I got it :D
The only working way I found to send a JavaScript file and run it is by declaring it in the index.html file inside the public folder ( if u made the app with create react app )
Normally react servers you this file and it adds it's components over it.
The reason why running the script inside a react class won't do ( as far as I managed to understand ) is that react isn't actually running JavaScript there but a language based on top of JavaScript ES6.
While this means that most functionalities you may be accustomed with are working in it there are exceptions too, mostly to new functionalities ( as the sensor's API, its pretty new ).
This may not be a problem in the future, but for the time being I guess this is one way to do it.
Edit * And #SakoBu's answer turned out to be the safe way of doing
this
( #Change my mind meme :3 )
I'm trying to add simple Vue components to a legacy app. The goal is to be able use some newer technologies without rewriting everything.
Basically I want to be able to put <tags-input> element wherever I want and have Vue replace it with a component.
I have instantiated Vue and added some <tags-input> elements to the html. I use them as parts of table rows generated originally by ASP WebForms.
import Vue from 'vue'
import TagsInput from './ClientTags'
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
import '../style.css';
Vue.config.productionTip = false
window.VueApp = new Vue({
el: '#app',
components: {
TagsInput
}
})
Initially the <tags-input> elements render nicely. The problem is that external legacy JS code adds new rows to the table (WebForms performs some auto-magic pagination). After the new rows are added to the DOM, Vue doesn't render <tags-input> elements inside them.
So, my goal is:
Whenever a new <tags-input> element is added to the DOM (by external JS), it should be rendered by Vue and added to the window.VueApp.
I finally figured it out (thanks to Sphinx comment). I probably should not be using Vue app at all in my case. Instead I should manually create and mount my components like so:
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
var MyComponent = Vue.component('my-component', {
// camelCase in JavaScript
props: ['someTitle'],
template: '<b>[XYZ {{ someTitle }}]</b> '
})
function dashToCamelCase( myStr ) {
return myStr.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
}
function getAttributes ( node ) {
var i,
attributeNodes = node.attributes,
length = attributeNodes.length,
attrs = {};
for ( i = 0; i < length; i++ ) attrs[dashToCamelCase(attributeNodes[i].name)] = attributeNodes[i].value;
return attrs;
}
function renderExisting(componentElementName, componentCreator){
$(componentElementName).each(function(){
var props = getAttributes(this)
var component = componentCreator(props)
component.$mount(this)
})
}
function renderNew(appElementId, componentElementName, componentCreator){
var obs = new MutationObserver(function(mutations, observer) {
$.each(mutations, function (i, mutation) {
var addedNodes = $(mutation.addedNodes);
var selector = componentElementName
var filteredEls = addedNodes.find(selector).addBack(selector);
filteredEls.each(function(){
var props = getAttributes(this)
var component = componentCreator(props)
component.$mount(this)
});
});
});
var canvasElement = $(appElementId)[0];
obs.observe(canvasElement, {childList: true, subtree: true});
}
function setUpRendering(appElementId, componentElementName, componentCreator){
renderExisting(componentElementName, componentCreator)
renderNew(appElementId, componentElementName, componentCreator)
}
$(function(){
setUpRendering('#myApp', 'my-component', (props) => new MyComponent({propsData: props}))
});
</script>
<script>
function addMyTag(){
$('#myApp').append( '<my-component some-title="' + (new Date()).getTime() + '"></my-component>' )
}
</script>
</head>
<body>
<button onclick='addMyTag()'>Add!</button>
<div id="myApp">
<my-component some-title="aaa"></my-component>
<my-component some-title="bbb"></my-component>
<my-component some-title="ccc"></my-component>
<div>
</body>
</html>
I will be happy to accept a better answer.
I have a sub component that does not need to be loaded immediately that I want to split out. I am trying to conditionally load in a react component via require.ensure. I am not getting any console errors but I am also not seeing anything being loaded. Here is the code I am calling :
renderContentzones() {
if (this.props.display ) {
return require.ensure([], () => {
const Component = require('./content-zones/component.jsx').default;
return (
<Component
content={this.props.display}
/>
);
});
}
return null;
}
It is just rendering a blank screen currently (no errors). This previously worked when I used import 'displayComponent' from './content-zones/component.jsx' and just returned it like you normally would in react, instead of this require.ensure but. Not sure what I am doing wrong here, any idea how to make something like this work? Thanks!
This is one way to do it, using the state to show the dynamic loaded component:
constructor(){
this.state = {cmp:null};
}
addComponent() {
const ctx = this;
require.ensure(['../ZonesComponent'], function (require) {
const ZonesComponent = require('../ZonesComponent').default;
ctx.setState({cmp:<ZonesComponent />});
});
}
render(){
return (
<div>
<div>Some info</div>
<div><button onClick={this.addComponent.bind(this)}>Add</button></div>
<div>
{this.state.cmp}
</div>
</div>
);
}
When you press the button add the component will be shown.
Hope this help.