How to combine JSX component with dangerouslySetInnerHTML - javascript

I'm displaying text that was stored in the database. The data is coming from firebase as a string (with newline breaks included). To make it display as HTML, I originally did the following:
<p className="term-definition"
dangerouslySetInnerHTML={{__html: (definition.definition) ? definition.definition.replace(/(?:\r\n|\r|\n)/g, '<br />') : ''}}></p>
This worked great. However there's one additional feature. Users can type [word] and that word will become linked. In order to accomplish this, I created the following function:
parseDefinitionText(text){
text = text.replace(/(?:\r\n|\r|\n)/g, '<br />');
text = text.replace(/\[([A-Za-z0-9'\-\s]+)\]/, function(match, word){
// Convert it to a permalink
return (<Link to={'/terms/' + this.permalink(word) + '/1'}>{word}</Link>);
}.bind(this));
return text;
},
I left out the this.permalink method as it's not relevant. As you can see, I'm attempting to return a <Link> component that was imported from react-router.However since it's raw HTML, dangerouslySetInnerHTML no longer works properly.
So I'm kind of stuck at this point. What can I do to both format the inner text and also create a link?

You could split the text into an array of Links + strings like so:
import {Link} from 'react-router';
const paragraphWithLinks = ({markdown}) => {
const linkRegex = /\[([\w\s-']+)\]/g;
const children = _.chain(
markdown.split(linkRegex) // get the text between links
).zip(
markdown.match(linkRegex).map( // get the links
word => <Link to={`/terms/${permalink(word)}/1`}>{word}</Link> // and convert them
)
).flatten().thru( // merge them
v => v.slice(0, -1) // remove the last element (undefined b/c arrays are different sizes)
).value();
return <p className='term-definition'>{children}</p>;
};
The best thing about this approach is removing the need to use dangerouslySetInnerHTML. Using it is generally an extremely bad idea as you're potentially creating an XSS vulnerability. That may enable hackers to, for example, steal login credentials from your users.
In most cases you do not need to use dangerouslySetHTML. The obvious exception is for integration w/ a 3rd party library, which should still be considered carefully.

I ran into a similar situation, however the accepted solution wasn't a viable option for me.
I got this working with react-dom in a fairly crude way. I set the component up to listen for click events and if the click had the class of react-router-link. When this happened, if the item has a data-url property set it uses browserHistory.push. I'm currently using an isomorphic app, and these click events don't make sense for the server generation, so I only set these events conditionally.
Here's the code I used:
import React from 'react';
import _ from 'lodash';
import { browserHistory } from 'react-router'
export default class PostBody extends React.Component {
componentDidMount() {
if(! global.__SERVER__) {
this.listener = this.handleClick.bind(this);
window.addEventListener('click', this.listener);
}
}
componentDidUnmount() {
if(! global.__SERVER__) {
window.removeEventListener("scroll", this.listener);
}
}
handleClick(e) {
if(_.includes(e.target.classList, "react-router-link")) {
window.removeEventListener("click", this.listener);
browserHistory.push(e.target.getAttribute("data-url"));
}
}
render() {
function createMarkup(html) { return {__html: html}; };
return (
<div className="col-xs-10 col-xs-offset-1 col-md-6 col-md-offset-3 col-lg-8 col-lg-offset-2 post-body">
<div dangerouslySetInnerHTML={createMarkup(this.props.postBody)} />
</div>
);
}
}
Hope this helps out!

Related

How do you get an HTML string from JSX without React. I'm using SolidJS for example

I'm not using React, so this won't work
ReactDomServer.renderToString(<div>p</div>)
I'm currently rendering the jsx in a hidden div with an id on the browser and then using
document.getElementById(id).outerHTML
to get the HTML, but I'm wondering if there's a more elegant solution
In SolidJS components actually render as plain DOM nodes, so you can actually just use all of the DOM nodes properties
In your specific example that could be something like
const SomeComponent = <div></div>;
console.log(SomeComponent.outerHTML) //this will output -> "<div></div>"
I hope this helps!
I'm currently rendering the jsx in a hidden div with an id on the browser and then using
document.getElementById(id).outerHTML
to get the HTML, but I'm wondering if there's a more elegant solution
JSX is not directly supported by the browser so it requires a compiler to be compiled into proper HTML that means you have to use a library like Solid, React ect.
In Solid, you don't need to render it into a hidden div, just don't output it to the DOM.
import { render, renderToString } from "solid-js/web";
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}
function App() {
let el = <Counter />
const increment = () => {
console.log((el.outerHTML));
};
return (
<button type="button" onClick={increment}>
get content
</button>
);
}
render(() => <App />, document.getElementById("app")!);
There is renderToString in Solid to support server side rendering, but it appears it does not work in browsers.

REACT JS: Dynamically convert part of paragraph into an anchor tag and on click of that anchor tag do an API call before doing a redirect

In React JSX I want to convert a part of the text into an anchor tag dynamically. Also on click of the anchor tag, I want to do some API call before redirecting it to the requested page. But I am failing to achieve this. Can anyone have a look and let me know where am I going wrong?
I have recreated the issue on code sandbox: here is the URL: Code Sandbox
Relevant code from sandbox:
import React from "react";
import "./styles.css";
export default function App() {
let bodyTextProp =
"This text will have a LINK which will be clicked and it will perform API call before redirect";
let start = 22;
let end = 26;
let anchorText = bodyTextProp.substring(start, end);
let anchor = `<a
href="www.test.com"
onClick={${(e) => handleClick(e)}}
>
${anchorText}
</a>`;
bodyTextProp = bodyTextProp.replace(anchorText, anchor);
const handleClick = (e) => {
e.preventDefault();
console.log("The link was clicked.");
};
const handleClick2 = (e) => {
e.preventDefault();
console.log("The link was clicked.");
};
return (
<div className="App">
<h3 dangerouslySetInnerHTML={{ __html: bodyTextProp }} />
<a href="www.google.com" onClick={(e) => handleClick2(e)}>
Test Link
</a>
</div>
);
}
The problem is variable scope. While it is entirely possible to use the dangerouslySetInnerHTML as you are doing, the onClick event isn't going to work the same way. It's going to expect handleClick to be a GLOBAL function, not a function scoped to the React component. That's because React doesn't know anything about the "dangerous" html.
Normally React is using things like document.createElement and addEventListener to construct the DOM and add events. And since it's using addEventListener, it can use the local function. But dangerouslySetInnerHTML bypasses all of that and just gives React a string to insert directly into the DOM. It doesn't know or care if there's an event listener, and doesn't try to parse it out or anything. Not really a good scenario at all.
The best solution would be to refactor your code so you don't need to use dangerouslySetInnerHTML.
*Edit: since you say that you need to do multiple replacements and simply splitting the string won't suffice, I've modified the code to use a split.
When used with a RegExp with a capturing group, you can keep the delimiter in the array, and can then look for those delimiters later in your map statement. If there is a match, you add an a
import React from "react";
import "./styles.css";
export default function App() {
let bodyTextProp =
"This text will have a LINK which will be clicked and it will perform API call before redirect";
let rx = /(\bLINK\b)/;
let array = bodyTextProp.split(rx);
const handleClick = (e) => {
console.log("The link was clicked.");
e.preventDefault();
};
const handleClick2 = (e) => {
e.preventDefault();
console.log("The link was clicked.");
};
return (
<div className="App">
<h3>
{array.map((x) => {
if (rx.test(x))
return (
<a href="www.test.com" onClick={handleClick}>
{x}
</a>
);
else return x;
})}
</h3>
<a href="www.google.com" onClick={(e) => handleClick2(e)}>
Test Link
</a>
</div>
);
}

Calling a Vue mixin directly with a constant?

I have a footer component with three links. When a user clicks a link, besides taking the user to a new page, I am trying to use a mixin to track the click event. When I set a breakpoint in chrome devtools, it appears that this implementation is not working. I imported my constants file, and the mixin.
footer, one link for brevity
<template>
<footer>
<div class="container">
<div class="row">
<div class="col align-center">
<a
href="/"
target="_blank"
class="btn"
name="item"
#click="logButtonClick(ANALYTICS.ITEM)">{{ $t('footer.item') }}</a>
</div>
</div>
</div>
</footer>
</template>
<script>
import analytics from '#/mixins/analytics'
import { ANALYTICS } from "#/constants"
export default {
name: 'PageFooter',
mixins: [analytics]
}
</script>
mixin
methods: {
logButtonClick (buttonType) { // breakpoint here, get nothing
this.$analytics.track({
identifier: `Consumer ${this.$options.name} - ${buttonType} Button`
})
}
}
Am I missing something? Should this implementation work or should I have a method such as:
methods: {
selectLink(str) {
if (str === item) {
this.logButtonClick(ANALYTICS.ITEM)
}
}
}
The original error I received was
"Property or method ANALYTICS not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class based components, by initializing the property."
and
"Uncaught TypeError: Cannot read property ITEM of undefined at click event...."
Essentially this means I did not define ANALYTICS either in PageFooter (because this is a dumb component, I did not want to add a data object to it, I wanted to keep it strictly presentational) or on the vue instance at a root level. Since ANALYTICS is undefined, ITEM then throws another error because it can not be a property of undefined.
This is my solution, I used a switch case in the and in the template tag added #click="selectLink('asd')"
methods: {
selectLink (str) {
switch (true) {
case str === 'asd':
this.logButtonClick(ANALYTICS.ITEM)
break
case str === 'efg':
this.logButtonClick(ANALYTICS.ITEM2)
break
case str === 'hij':
this.logButtonClick(ANALYTICS.ITEM3)
break
}
}
}
and the unit test:
it('[positive] should track analytics if `asd` is passed to selectLink()', () => {
const str = 'asd'
const mockFn = jest.fn()
jest.spyOn(wrapper.vm, 'logButtonClick')
wrapper.find('a').trigger('click')
mockFn(str)
expect(wrapper.vm.logButtonClick).toHaveBeenCalledWith(ANALYTICS.COOKIE_CONSENT)
})
https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties
Moral of the story, question things when senior engineers tell you to do something funky in a code review.

Why and how to use classnames utility in React components?

Could you please explain to me in a simple way what is the purpose of using Classnames utility in React code? I've just read Classnames docs, but I still can't grasp what is the main reason to use it in code like so:
import classnames from 'classnames';
[...]
render() {
const { className } = this.props
return (
<div className={classnames('search', className)}>
<div className="search-bar-wrapper">
<form className="search-form" onSubmit={this.onSearch.bind(this)}>
<div className="search-bar">
<label className="screen-reader-only" htmlFor="header-search-form">Search</label> [...]
Full version of this code (jsx):
https://jsfiddle.net/John_Taylor/j8d42m2f/2/
I don't understand what is going on this line of code:
<div className={classnames('search', className)}>
I've also read that (
how to use classnames library for React.js ) answer, but I still have problems with understanding my code snippet.
classnames library lets you join different classes based on different conditions in a simpler way.
Suppose you have 2 classes of which one is going to get used every time but the second one gets used based on some condition. So without classnames library you would something like this
render() {
const classnames = 'firstClass';
if (someCondition) {
classnames += ' secondClass'
}
return(
<input className={classnames} .. />
);
}
But with classnames library you would do that in this way
render() {
const classnames = {'firstClass', {'secondClass': someCondition}}
return(
<input className={classnames} .. />
);
}
In your case, <div className={classnames('search', className)}>is equivalent to <div className={`search ${className}`}>.
classnamesis mainly useful when you have to deal with conditional classes.
Classnames make it easy to apply class names to react component conditionally.
For example: Let create a state and apply a class to the button component when the button is clicked
const [isActive, setActive] = setState(false);
import classnames from "classnames"
var buttonClasses = classnames({
"btn": true,
"btn__active": isActive;
)}
<button className={buttonClasses} onClick={() => setActive(!isActive)}>Make me active</button>
This code will apply the "isActive" class to the button when it is clicked.
I hope this help answer your question
If used the way you use it in your script classnames simply joins the strings you give it with spaces.
const className = 'x';
const result = classnames('search', className);
// result == 'search x'
I think you should attempt reading the docs one more time, they are very clear. Specifically, this part:
So where you may have the following code to generate a className prop for a in React:
var Button = React.createClass({
// ...
render () {
var btnClass = 'btn';
if (this.state.isPressed) btnClass += ' btn-pressed';
else if (this.state.isHovered) btnClass += ' btn-over';
return <button className={btnClass}>{this.props.label}</button>;
}
});
You can express the conditional classes more simply as an object:
var classNames = require('classnames');
var Button = React.createClass({
// ...
render () {
var btnClass = classNames({
btn: true,
'btn-pressed': this.state.isPressed,
'btn-over': !this.state.isPressed && this.state.isHovered
});
return <button className={btnClass}>{this.props.label}</button>;
}
});
The sole purpose of the library here is to remove the need of writing ugly code to dynamically add classes to something, and just use a consistent syntax instead.
Another example:
var Button = React.createClass({
render () {
return <button className={this.props.foo ? 'bar' : 'baz' + this.props.bar ? 'baz' : 'foo'}>{this.props.label}</button>;
}
});
That is really hard to read. There is a lot of code out there that looks similar to that.
Here is a better way, using classnames:
var classes = classNames({
bar: this.props.foo,
baz: !this.props.foo,
active: this.props.bar,
hover: !this.props.bar
});
var Button = React.createClass({
render () {
return <button className={classes}>{this.props.label}</button>;
}
});
It's very clear there what's happening. If the values in the object are true, the key will be appended to the class. So in that example, the final class will be bar active, given this.props.foo is truthy and this.props.bar is truthy.
Your code snippet
<div className={classnames('search', className)}>
is equivalent to:
<div className={`search ${className ? className : ""}`}>
so in this particular case it's just encapsulates ternary operator from inside template string. Which is small but improvement - harder to produce bug in.
Probably it is simplest case of using classnames, however when you'll have more and more conditions around your class name manipulations, it is going to be more and more useful

How to make React and Meteor fit together

I don't know if it's more a React or Meteor concern, maybe both, but I am currently building a web app with these two frameworks and I am facing a programmer issue. I am not a Javascript developer but a Java developer (I use daily GWT), so maybe I made some rookie mistakes.
My app keeps growing and I have more and more React components, about twenty or so. Now that I have a view that is fine, I have added some functionalities to my components but it turns up I add more and more logic in the react components which is, I believe, against MVC principles.
However, I don't know how to move the logic in "Meteor controller components". Right now, I use Meteor for its model and that's just about all. I saw many times this Pete Hunt's talk and how he built his application but it has only one 'simple' component.
In fact, without React, the view would be in html files, defined with templates. The controller would be in js files and the logic will appear to be there. I can clearly see the split between the view and the controller.
Html file (from leaderboard example):
<template name="leaderboard">
...
</template>
<template name="player">
<div class="player {{selected}}">
...
</div>
</template>
Javascript file (from leaderboard example):
...
Template.leaderboard.players = function () {
return Players.find({}, {sort: {score: -1, name: 1}});
};
Template.leaderboard.selected_name = function () {
var player = Players.findOne(Session.get("selected_player"));
return player && player.name;
};
...
Since React is javascript, it's really easy and tempting to put all we want in React components.
I am aware of these frameworks are relatively new for everybody but I wonder wheter some conventions exist about how to design such an MVC application in order to have a flexible and maintainable web application, any guidelines to follow? I am not looking for the 'best' way to do it but for some opinions.
Note: I deliberately didn't put a lot of code here to not be focus on it but feel free to illustrate your answer with whatever you want (code, schema, links...).
Here is an example of what I am doing. In this example, everything is done in react classes, maybe it's a the best way to do it, maybe not, I need your thoughts.
To sum up, it creates a list of elements (Boostrap list group) from an array given as input (something like [{name: itemName1, type:itemType1}, {name: itemName2, type:itemType2} ...] which generates a view like:
itemName1
itemName2
...
Each item as its own style according to its type. Then through the input text box, user can make a search trough this list, it filters the list and generates a new one that composed with the matching elements (the search algorithm is not right and will be changed). Plus, there are additional commands with certain keyboard key. Everything works fine but as you can notice, all is in react classes, I don't figure out how to fit Meteor with React.
Meteor file:
if (Meteor.isClient) {
Meteor.startup(function() {
//Build the view
React.renderComponent(
<Search items={initialItems}/>,
document.getElementById('main')
);
});
}
React file:
Search = React.createClass({
getInitialState : function() {
return (
{
items : flat(this.props.items),
active : 0
}
);
},
setListVisibility: function(visibility) {
this.refs.list.getDOMNode().style.visibility = visibility;
},
onchangeHandler: function() {
var re = new RegExp(this.refs.search.getDOMNode().value, "i");
var res = [];
//filter on props.items and not state.items
flat(this.props.items).map(function(item){
if(item.name.search(re) >= 0)
res.push(item);
});
this.setState({ items : res, active : 0});
},
onkeydownHandler: function(event){
if(event.key == "ArrowDown" || event.key == "ArrowUp"){
var shift = event.key == "ArrowDown" ? 1 : -1;
var newActive = this.state.active + shift;
if(newActive >= 0 && newActive < this.state.items.length)
this.setState({ active : this.state.active + shift });
} else if(event.key == "ArrowRight"){
if(this.state.items.length > 0){
var item = this.state.items[this.state.active];
var newItems = retrieveItem(this.props.items, item.name, typeToSubType[item.type]);
newItems = flat(newItems);
if(newItems.length > 0)
this.setState({ items : newItems, active : 0 });
}
} else if(event.key == "ArrowLeft"){
this.setState({ items : flat(this.props.items), active : 0});
} else if(event.key == "Enter"){
if(this.state.items.length > 0){
var item = this.state.items[this.state.active];
console.log("Add "+item.name+" "+item.type+" to the view");
}
}
},
render: function () {
return (
<div>
<nav className="navbar navbar-default" role="navigation">
<div className="container-fluid">
<div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<form className="navbar-form navbar-left" role="search">
<div className="form-group">
<input ref="search" type="text" className="form-control" placeholder="Search" size="100"
onChange={this.onchangeHandler}
onKeyDown={this.onkeydownHandler}
onFocus={this.setListVisibility.bind(this, "visible")}
onBlur={this.setListVisibility.bind(this, "hidden")}/>
</div>
</form>
</div>
</div>
</nav>
<List ref="list" items={this.state.items} active={this.state.active}/>
</div>
);
}
});
List = React.createClass({
render: function () {
var createItem = function(item, index) {
var cl = "list-group-item";
if(index == this.props.active)
cl += " active";
var gly = "glyphicon ";
switch(item.type){
case "dimension":
gly += "glyphicon-certificate";
break;
case "hierarchy":
gly += "glyphicon-magnet";
break;
case "level":
gly += "glyphicon-leaf";
break;
case "measure":
gly += "glyphicon-screenshot";
break;
}
return (<a href="#" className={cl} key={item.type+"/"+item.name}>
<span className={gly}></span>{" "}{item.name}
</a>);
};
return (
<div className="list-group search-list">
{this.props.items.map(createItem, this)}
</div>
);
}
});
Your approach is sound: Meteor for the model and React for the View and ViewController.
Anything functionality that has nothing to do with presentation should be in the model (business logic, validation rules, etc).
Anything to do with presentation and responding to user input should be in React (input, validation output, etc).
Today you could consider this nice peace of code:
https://github.com/reactjs/react-meteor
This repository defines a Meteor package that automatically integrates the React rendering framework on both the client and the server, to complement or replace the default Handlebars templating system.
As of Meteor 1.3 there is an officially supported way to integrate React with Meteor. So for anyone stumbling across this question today, here's the Meteor 1.3+ answer:
To use React as Meteor's view layer, first add the React packages from npm:
meteor npm install --save react react-dom
Now you can simply import React and use it in your project. To render a simple React component, create a simple HTML container:
client/main.html
<body>
<div id="app"></div>
</body>
And render your React component in it:
client/main.jsx
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
class HelloWorld extends React.Component {
render() {
return <p>Hello World!</p>;
}
}
Meteor.startup(() => {
render(<HelloWorld />, document.getElementById('app'));
});
To use reactive Meteor data sources such as Minimongo collections within React components, you should use the react-meteor-data package.
Read more in the official Meteor guide

Categories