ClipboardJS with React, using document.getElementById() - dom

Originally, I had it working fine.
Then I did this and now I can't get it to work
ClipboardField.js
import React from 'react';
export default (props) => {
return(
<div id="clip" data-clipboard-text={props.code} onClick={props.onClick}>
<p> Copy to clipboard.</p>
</div>
);
}
Field.js
class DashWizardTwoCore extends Component {
componentDidMount(){
const btns = document.getElementById('clip');
const clipboard = new Clipboard(btns);
}
componentDidUpdate() {
clipboard.on('success', function(e) {
console.log(e);
});
clipboard.on('error', function(e) {
console.log(e);
});
}
render(){
const someCode = "const foo = 1"
return (
<div>
<ClipboardField code={this.someCode} /> }
</div>
);
}
}
If you take the code out of ClipboardField and into Field it works. It's mostly, how do I use document.getElementById() in my parent component to find something in my child?
Their examples:
https://github.com/zenorocha/clipboard.js/blob/master/demo/constructor-selector.html#L18
https://github.com/zenorocha/clipboard.js/blob/master/demo/constructor-node.html#L16-L17
https://github.com/zenorocha/clipboard.js/blob/master/demo/constructor-nodelist.html#L18-L19

Your code is fine you just have a few issues:
you are binding clipboard.on in componentDidUpdate which won't trigger here since you are not really changing anything (in the ClipboardField component) that triggers this event.
You are passing {this.someCode} in the code prop which would be undefined should just be {someCode}
So it's just a matter of moving your clipboard.on to the componentDidMount right after the new Clipboard and use code={someCode}
https://jsfiddle.net/yy8cybLq/
--
In React whenever you want to access the actual dom element of your component we use what react calls as refs, I would suggest you do this rather than using getElementById as this is the "best practice".
However stateless components (like your ClipboardField component above) can't have refs so you just need to change it to be a normal component.
Here's a fiddle with your code but using refs instead: https://jsfiddle.net/e5wqk2a2/
I tried including links to the react docs for stateless components and refs but apparently don't have enough "rep" to post more than 2 links, anyways quick google search should point you in the right direction.

I adjusted your code and created a simple integration of clipboard.js with React.
Here's the fiddle: https://jsfiddle.net/mrlew/ehrbvkc1/13/ . Check it out.

Related

Using a Vue3 component as a Leaflet popup

This previous SO question shows how we can use a Vue2 component as the content of a LeafletJS popup. I've been unable to get this working with Vue3.
Extracting the relevant section of my code, I have:
<script setup lang="ts">
import { ref } from 'vue'
import L, { type Content } from 'leaflet'
import type { FeatureCollection, Feature } from 'geojson'
import LeafletPopup from '#/components/LeafletPopup.vue'
// This ref will be matched by Vue to the element with the same ref name
const popupDialogElement = ref(null)
function addFeaturePopup(feature:Feature, layer:L.GeoJSON) {
if (popupDialogElement?.value !== null) {
const content:Content = popupDialogElement.value as HTMLElement
layer.bindPopup(() => content.$el)
}
}
</script>
<template>
<div class="map-container">
<section id="map">
</section>
<leaflet-popup ref="popupDialogElement" v-show="false">
</leaflet-popup>
</div>
</template>
This does produce a popup when I click on the map, but it has no content.
If, instead, I change line 14 to:
layer.bindPopup(() => content.$el.innerHTML)
then I do get a popup with the HTML markup I expect, but unsurprisingly I lose all of the Vue behaviours I need (event handling, etc).
Inspecting the addFeaturePopup function in the JS debugger, the content does seem to be an instance of HTMLElement, so I'm not sure why it's not working to pass it to Leaflet's bindPopup method. I assume this has something to do with how Vue3 handles references, but as yet I can't see a way around it.
Update 2022-06-09
As requested, here's the console.log output: I've put it in a gist as it's quite long
So just to document the solution I ended up using, I needed to add an additional style rule in addition to the general skeleton outlined in the question:
<style>
.leaflet-popup-content >* {
display: block !important;
}
</style>
This overrides the display:none that is attached to the DOM node by v-show=false. It would be nice not to need the !important, but I wasn't able to make the rule selective enough in my experiments.

How to seperate Vue logic in a laravel app based on layout and page templates

I have a laravel app and a Vue instance attached to the body (or a div, just inside the body).
const app = new Vue({
el: '#app'
});
I think it makes sense to use the Vue instance for stuff relating to the layout (eg header, nav, footer logic).
Now I have a form that is visible on a specific route (e.g. example.com/thing/create). I want to add some logic to it, e.g. hiding a field based on selected option in the form. It is logic meant for just this form (not to be reused). I prefer not to put all the logic inline with the form but put it in the app.js. I could put it in the Vue instance bound to the body but that sounds odd as it only applies to the form that is much deeper into the dom.
I want to leave the markup of the form in the blade template (that inherits the layout).
I tried creating a component but am not sure how to bind this inside the main Vue instance. What is the best way to handle things for this form, put it in the app.js and have it somewhat structured, putting the variables somewhat into scope. Or is it really necessary to remove the main Vue instance bound to the full layout code?
What I tried was something like this, but it does not work (attaching it to the <form id="object-form"> seems to fail:
var ObjectForm = {
template: function() { return '#object-form'},
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
}
};
Things do work if I remove the #app Vue instance or when I put everything directly in the app Vue instance. But that seems messy, if I have similar variables for another form they should be seperated somewhat.
I would appreciate some advice regarding the structure (differentiate page layout and page specific forms) and if possible some example to put the form logic inside the main app.js.
I hope this helps kind of break things down for you and helps you understand Vue templating.
It is best to take advantage of Vue's components. For you it would look something like this. Some of this code depends on your file structure, but you should be able to understand it.
In your app.js file (or your main js file)
Vue.component('myform',require('./components/MyForm.vue'));
const app = new Vue({
el: "#app"
});
Then create the MyForm.vue file
<template>
<form>
Put Your Form Markup Here
</form>
</template>
<script>
// Here is where you would handle the form scripts.
// Use methods, props, data to help set up your component
module.exports = {
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
},
methods: {
// add methods for dynamically changing form values
}
}
</script>
Then you will be able to just call in your blade file.
<myform></myform>
I found out how to do it. The trick was to use an inline template. Surround the form in the view with:
<object-form inline-template>
<form>...</form>
</object-form>
Where object-form is the name of the component. In the ObjectForm code above I remove the template, like this:
var ObjectForm = {
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
}
};
I attach the component within the root vue app like this:
const app = new Vue({
el: 'body',
components: {
'object-form': ObjectForm
}
});
This way I can use the form as it was generated from the controller and view and I can separate it from the root (attached to body) methods and properties.
To organize it even better I can probably store the ObjectForm in a seperate .vue file the way #Tayler Foster suggested.

Changing the document title in React?

I'm trying to update the title of the document in a React app. I have very simple needs for this. The title is essentially used to put the Total component on display even when you're on a different tab.
This was my first instinct:
const React = require('react');
export default class Total extends React.Component {
shouldComponentUpdate(nextProps) {
//otherstuff
document.title = this.props.total.toString();
console.log("Document title: ", document.title);
return true;
}
render() {
document.title = this.props.total;
return (
<div className="text-center">
<h1>{this.props.total}</h1>
</div>
);
}
}
I thought this would just update the document.title every time this component was rendered, but it doesn't appear to do anything.
Not sure what I'm missing here. Probably something to do with how React runs this function - maybe somewhere that the document variable isn't available?
EDIT:
I'm starting a bounty for this question, as I still haven't found any solution. I've updated my code to a more recent version.
A weird development is that the console.log does print out the title I'm looking for. But for some reason, the actual title in the tab isn't updating. This issue is the same across Chrome, Safari, and Firefox.
I now use react-helmet for this purpose, as it allows to customize different meta tags and links, and it also supports SSR.
import { Helmet } from 'react-helmet'
const Total = () => (
<div className="text-center">
<Helmet>
<meta charSet="utf-8" />
<title>{this.props.total}</title>
</Helmet>
<h1>{this.props.total}</h1>
</div>
)
Original answer: there's actually a package by gaeron for this purpose, but in a declarative way:
import React, { Component } from 'react'
import DocumentTitle from 'react-document-title'
export default class Total extends Component {
render () {
return (
<DocumentTitle title={this.props.total}>
<div className='text-center'>
<h1>{this.props.total}</h1>
</div>
</DocumentTitle>
)
}
}
Inside your componentDidMount() function in App.js (or wherever), simply have:
componentDidMount() {
document.title = "Amazing Page";
}
The reason this works is anywhere in your react project you have access to the Js global scope. Go ahead and type window in your sites console. Basically everything there you will be able to access in your React project.
I think webpack-dev-server runs in an iframe mode by default:
https://webpack.github.io/docs/webpack-dev-server.html#iframe-mode
So that might be why your attempts to set the title are failing. Try setting the inline option to true on webpack-dev-server, if you haven't already.
If the react-document-title package isn't working for you, the quick'n'dirty way to do that would be in a lifecycle method, probably both componentDidMount and componentWillReceiveProps (you can read more about those here):
So you would do something like:
const React = require('react');
export default class Total extends React.Component {
// gets called whenever new props are assigned to the component
// but NOT during the initial mount/render
componentWillReceiveProps(nextProps) {
document.title = this.props.total;
}
// gets called during the initial mount/render
componentDidMount() {
document.title = this.props.total;
}
render() {
return (
<div className="text-center">
<h1>{this.props.total}</h1>
</div>
);
}
}
There is a better way of dynamically changing document title with react-helmet package.
As a matter of fact you can dynamically change anything inside <head> tag using react-helmet from inside your component.
const componentA = (props) => {
return (
<div>
<Helmet>
<title>Your dynamic document/page Title</title>
<meta name="description" content="Helmet application" />
</Helmet>
.....other component content
);
}
To change title, meta tags and favicon dynamically at run time react-helmet provides a simple solution. You can also do this in componentDidMount using the standard document interface. In the example below I am using the same code for multiple sites, so helmet is looking for favicon and title from an environment variable
import { Helmet } from "react-helmet";
import { getAppStyles } from '../relative-path';
import { env } from '../relative-path';
<Helmet>
<meta charSet="utf-8" />
<title>{pageTitle[env.app.NAME].title}</title>
<link rel="shortcut icon" href={appStyles.favicon} />
</Helmet>

Is it fine to mutate attributes of React-controlled DOM elements directly?

I'd like to use headroom.js with React. Headroom.js docs say:
At it's most basic headroom.js simply adds and removes CSS classes from an element in response to a scroll event.
Would it be fine to use it directly with elements controlled by React? I know that React fails badly when the DOM structure is mutated, but modifying just attributes should be fine. Is this really so? Could you show me some place in official documentation saying that it's recommended or not?
Side note: I know about react-headroom, but I'd like to use the original headroom.js instead.
EDIT: I just tried it, and it seems to work. I still don't know if it will be a good idea on the long run.
If React tries to reconcile any of the attributes you change, things will break. Here's an example:
class Application extends React.Component {
constructor() {
super();
this.state = {
classes: ["blue", "bold"]
}
}
componentDidMount() {
setTimeout(() => {
console.log("modifying state");
this.setState({
classes: this.state.classes.concat(["big"])
});
}, 2000)
}
render() {
return (
<div id="test" className={this.state.classes.join(" ")}>Hello!</div>
)
}
}
ReactDOM.render(<Application />, document.getElementById("app"), () => {
setTimeout(() => {
console.log("Adding a class manually");
const el = document.getElementById("test");
if (el.classList)
el.classList.add("grayBg");
else
el.className += ' grayBg';
}, 1000)
});
And here's the demo: https://jsbin.com/fadubo/edit?js,output
We start off with a component that has the classes blue and bold based on its state. After a second, we add the grayBg class without using React. After another second, the component sets its state so that the component has the classes blue, bold, and big, and the grayBg class is lost.
Since the DOM reconciliation strategy is a black box, it's difficult to say, "Okay, my use case will work as long as React doesn't define any classes." For example, React might decide it's better to use innerHTML to apply a large list of changes rather than setting attributes individually.
In general, if you need to do manual DOM manipulation of a React component, the best strategy is to wrap the manual operation or plugin in its own component that it can 100% control. See this post on Wrapping DOM Libs for one such example.

Create an instance of a React class from a string

I have a string which contains a name of the Class (this is coming from a json file). This string tells my Template Class which layout / template to use for the data (also in json). The issue is my layout is not displaying.
Home.jsx:
//a template or layout.
var Home = React.createClass({
render () {
return (
<div>Home layout</div>
)
}
});
Template.jsx:
var Template = React.createClass({
render: function() {
var Tag = this.props.template; //this is the name of the class eg. 'Home'
return (
<Tag />
);
}
});
I don't get any errors but I also don't see the layout / Home Class. I've checked the props.template and this logs the correct info. Also, I can see the home element in the DOM. However it looks like this:
<div id='template-holder>
<home></home>
</div>
If I change following line to:
var Tag = Home;
//this works but it's not dynamic!
Any ideas, how I can fix this? I'm sure it's either simple fix or I'm doing something stupid. Help would be appreciated. Apologies if this has already been asked (I couldn't find it).
Thanks,
Ewan
This will not work:
var Home = React.createClass({ ... });
var Component = "Home";
React.render(<Component />, ...);
However, this will:
var Home = React.createClass({ ... });
var Component = Home;
React.render(<Component />, ...);
So you simply need to find a way to map between the string "Home" and the component class Home. A simple object will work as a basic registry, and you can build from there if you need more features.
var components = {
"Home": Home,
"Other": OtherComponent
};
var Component = components[this.props.template];
No need to manually map your classes to a dictionary, or "registry", as in Michelle's answer. A wildcard import statement is already a dictionary!
import * as widgets from 'widgets';
const Type = widgets[this.props.template];
...
<Type />
You can make it work with multiple modules by merging all the dictionaries into one:
import * as widgets from 'widgets';
import * as widgets2 from 'widgets2';
const registry = Object.assign({}, widgets, widgets2);
const widget = registry[this.props.template];
I would totally do this to get dynamic dispatch of react components. In fact I think I am in a bunch of projects.
I had the same problem, and found out the solution by myself. I don't know if is the "best pratice" but it works and I'm using it currently in my solution.
You can simply make use of the "evil" eval function to dynamically create an instance of a react component. Something like:
function createComponent(componentName, props, children){
var component = React.createElement(eval(componentName), props, children);
return component;
}
Then, just call it where you want:
var homeComponent = createComponent('Home', [props], [...children]);
If it fits your needs, maybe you can consider something like this.
Hope it helps.
I wanted to know how to create React classes dynamically from a JSON spec loaded from a database and so I did some experimenting and figured it out. My basic idea was that I wanted to define a React app through a GUI instead of typing in code in a text editor.
This is compatible with React 16.3.2. Note React.createClass has been moved into its own module.
Here's condensed version of the essential parts:
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import createReactClass from 'create-react-class'
const spec = {
// getDefaultProps
// getInitialState
// propTypes: { ... }
render () {
return React.createElement('div', null, 'Some text to render')
}
}
const component = createReactClass(spec)
const factory = React.createFactory(component)
const instance = factory({ /* props */ })
const str = ReactDOMServer.renderToStaticMarkup(instance)
console.log(str)
You can see a more complete example here:
https://github.com/brennancheung/02-dynamic-react/blob/master/src/commands/tests/createClass.test.js
Here is the way it will work from a string content without embedding your components as statically linked code into your package, as others have suggested.
import React from 'react';
import { Button } from 'semantic-ui-react';
import createReactClass from 'create-react-class';
export default class Demo extends React.Component {
render() {
const s = "return { render() { return rce('div', null, rce(components['Button'], {content: this.props.propA}), rce(components['Button'], {content: 'hardcoded content'})); } }"
const createComponentSpec = new Function("rce", "components", s);
const componentSpec = createComponentSpec(React.createElement, { "Button": Button });
const component = React.createElement(createReactClass(componentSpec), { propA: "content from property" }, null);
return (
<div>
{component}
</div>
)
}
}
The React class specification is in string s. Note the following:
rce stands for React.createElement and given as a first param when callingcreateComponentSpec.
components is a dictionary of extra component types and given as a second param when callingcreateComponentSpec. This is done so that you can provide components with clashing names.
For example string Button can be resolved to standard HTML button, or button from Semantic UI.
You can easily generate content for s by using https://babeljs.io as described in https://reactjs.org/docs/react-without-jsx.html. Essentially, the string can't contain JSX stuff, and has to be plain JavaScript. That's what BabelJS is doing by translating JSX into JavaScript.
All you need to do is replace React.createElement with rce, and resolve external components via components dictionary (if you don't use external components, that you can skip the dictionary stuff).
Here is equivalent what in the code above. The same <div> with two Semantic UI Buttons in it.
JSX render() code:
function render() {
return (
<div>
<Button content={this.props.propA}/>
<Button content='hardcoded content'/>
</div>
);
}
BabelJS translates it into:
function render() {
return React.createElement("div", null, React.createElement(Button, {
content: this.props.propA
}), React.createElement(Button, {
content: "hardcoded content"
}));
}
And you do replacement as outlined above:
render() { return rce('div', null, rce(components['Button'], {content: this.props.propA}), rce(components['Button'], {content: 'hardcoded content'})); }
Calling createComponentSpec function will create a spec for React class.
Which then converted into actual React class with createReactClass.
And then brought to life with React.createElement.
All you need to do is return it from main component render func.
When you use JSX you can either render HTML tags (strings) or React components (classes).
When you do var Tag = Home, it works because the JSX compiler transforms it to:
var Template = React.createElement(Tag, {});
with the variable Tag in the same scope and being a React class.
var Tag = Home = React.createClass({
render () {
return (
<div>Home layout</div>
)
}
});
When you do
var Tag = this.props.template; // example: Tag = "aClassName"
you are doing
var Template = React.createElement("aClassName", null);
But "aClassName" is not a valid HTML tag.
Look here