Render or Use DocumentFragment in a React Component - javascript

I have a requirement in my React-based application to render dynamic forms. The form definitions are stored as JSON documents and I already have a JS library that parses the definitions and returns a DocumentFragment. This library is used in other non-React applications as well so I cannot change it.
To avoid re-writing the entire logic in my React application to parse the definitions and render the forms, I want to use the existing library.
My question is, what would be the best way to render the DocumentFragment in a React component?
Here is my DocumentFragment if I just output it to the console in my render() method.
#document-fragment
<fieldset id="metadata-form-908272" class="metadata-form-rendition hide-pages">
<div class="page-header-row">
<div class="page-header-cell">
<span>[Un-named page]</span>
<button class="page-header-button button icon">
<span class="icon icon-arrow-up-11"></span></button>
</div>
</div>
<div class="page-area" id="metadata-form-page-001-area">
<div class="question-row">
<div class="question-label-cell mandatory">I have read and understood my obligations:</div>
<div class="question-input-cell">
<div class="validation-message"></div>
<label><input type="radio" value="Yes" name="metadata-form-908272-question-1">
<span>Yes</span></label>
<label><input type="radio" value="No" name="metadata-form-908272-question-1">
<span>No</span></label>
</div>
</div>
<div class="question-row">
<div class="question-label-cell">Please state all sources for the information provided:</div>
<div class="question-input-cell">
<div class="validation-message"></div>
<div class="formatted-editor">
<div class="editor-area" contenteditable="true">
<p>​</p>
</div>
</div>
</div>
</div>
</div>
</fieldset>

Update: Security Alert!
Thanks for reminding from #JaredSmith in comment, the method provided here
really has security issue. It's not proper to apply it if the library is not from your internal.
To learn more about the issue, you could look into the link of dangerouslysetinnerhtml I referred below.
Here is indeed a tricky way to achieve your goal. By the information you provided in comment:
... I call theExternalLibrary.getFormFragment({some_data}) ...
cause that DocumentFragments only in memory, maybe as you know, we need to append the fragment to a real DOM element first, so let's just create a root element for appending:
let rootElement = document.createElement("div");
let frag = theExternalLibrary.getFormFragment({some_data});
rootElement.appendChild(frag);
Now we have a pure JavaScript elements DOM tree here. In order to convert it to React elements, here is the way which involves a method that react provides: dangerouslysetinnerhtml
You could see that this method is not encouraged to use by its scary name.
render() {
let rootElement = document.createElement("div");
let frag = theExternalLibrary.getFormFragment({some_data});
rootElement.appendChild(frag);
// rootElement.innerHTML is in string type.
return <div dangerouslySetInnerHTML={{ __html: rootElement.innerHTML }} />;
}
Live example:

I took a crack at this myself, as DOMPurify and the upcoming Sanitiser API work best when returning DocumentFragments:
function getDocumentFragment(text) {
const f = document.createDocumentFragment();
const p = document.createElement("p");
p.textContent = text;
f.appendChild(p);
return f;
}
class FragmentRenderer extends React.Component {
constructor(props) {
super(props);
this.setRef = this.setRef.bind(this);
}
setRef(ref) {
this.ref = ref;
}
componentDidMount() {
this.appendFragment();
}
componentDidUpdate(prevProps) {
if (prevProps.text != this.props.text) {
this.appendFragment();
}
}
appendFragment() {
if (!this.ref) {
return;
}
while (this.ref.firstChild) {
this.ref.removeChild(this.ref.firstChild);
}
this.ref.appendChild(getDocumentFragment(this.props.text));
}
render() {
return React.createElement("div", {
ref: this.setRef
});
}
}
ReactDOM.render(React.createElement(FragmentRenderer, {
text: "Just like this"
}), document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
So now you're not casting to a string and using dangerouslySetInnerHTML, which can in extreme cases lead to the elements you write being different to the elements in the original fragment, due to parsing issues. However, you do still need to trust whereever this fragment came from - it cannot come from a user-controlled source. DOMPurify or the Sanitizer API will be your best friends here.

Related

Why props alone are being used in called React function component?

I was learning React and I came to a point which created confusion. Everywhere I was using props while writing Function components.
I always use props.profile and it works fine. But in one code component, I had to write
const profiles=props; and it worked fine.
I tried using const profiles=props.profile; and also I tried using inside return in 'Card' function component
{props.profile.avatar_url} but both of them failed
Below is my code which works fine
const Card=(props)=>{
const profiles=props; //This I dont understand
return(
<div>
<div>
<img src={profiles.avatar_url} width="75px" alt="profile pic"/>
</div>
<div>
<div>{profiles.name}</div>
<div>{profiles.company}</div>
</div>
</div>
);
}
const CardList=(props)=>{
return(
<div>
{testDataArr.map(profile=><Card {...profile}/>)}
</div>
);
}
Can someone please help me understand why I can't use const profiles=props.profile?
What are the other ways to achieve the correct result?
Your testDataArr might be this,
testDataArr = [{avatar_url:"",name:"",company:""},{avatar_url:"",name:"",company:""},{avatar_url:"",name:"",company:""}]
Now when you do this,
{testDataArr.map(profile=><Card {...profile}/>)}
here profile = {avatar_url:"",name:"",company:""},
and when you do,
<Card {...profile}/>
is equivalent to,
<Card avatar_url="" name="" company=""/>
In child component, when you do this,
const profiles=props;
here props = {avatar_url:"",name:"",company:""}
So you can access it's values,
props.avatar_url
props.name
props.company
But when you do this,
const profiles=props.profile
profile key is not present in {avatar_url:"",name:"",company:""} object and it fails.
OK. Here is the issue, the props object does not contain a profile attribute, but IT IS the profile attribute. Becouse you are spreading the profile variable when you render the Card element (in the CardList), you basically are writing:
<Card avatarUrl={profile.avatarUrl} comapny={profile.comany} />
Instead, you should do
<Card profile={profile} />
and then in your Card component access the data this way
const Card = (props) => {
const profile = props.profile
}
or even simpler
const Card = ({profile}) => {
return <div>{profile.comany}</div>
}

Parse nested HTML that's within a string in React?

I'm building a type ahead feature in React.
I have wrapper component that has an array of objects, and it renders item; which's a stateless component.
So, suppose I have const name= 'Hasan'. Which gets parsed to >> const parsedName = Ha<span>san</span>; assuming the term to search for is san.
I have tried dangerouslySetInnerHTML={{ __html: parsedName }} attribute on the parent element, but it didn't work.
With plain html this would be: el.innerHTML = parsedName
The goal is to style the span as desired. Any ideas?
class Test extends React.Component {
render() {
const name = 'san';
const parsedName = name.replace(new RegExp('san', 'ig'), '<span>span</span>');
return (
<div dangerouslySetInnerHTML={{__html: parsedName}}/>
);
}
}
ReactDOM.render(
<Test/>,
document.getElementById('container')
);
<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="container">
</div>
Without code its hard to tell what's wrong. I have created a working snippet here that might help you debug your issue.
Updated the example based on the comment.

Create elements dynamically within React component

I have created a helper function that creates elements dynamically within my component when I click a button. However it's not displaying half of the html I'm trying to append to the parent div.
It adds the label correctly as html, but the rest is just in plain text. Can anyone see why?
The function used to dynamically create content:
function addElement(parentId, elementTag, elementId, html) {
let parentElement = document.getElementById(parentId);
let elementToAdd = document.createElement(elementTag);
elementToAdd.setAttribute('id', elementId);
elementToAdd.innerHTML = html;
parentElement.appendChild(elementToAdd);
}
My function within my component:
static addMatch() {
let html = "<div className=\"form-group\"><label className=\"control-label\">Add Match</label>" +
"<DatePickerselected={this.state.startDate}onChange={this.handleChange.bind(this)}/></div>";
addElement('fixture-parent', 'newMatch', uuid(), html);
}
My full react component is below:
import React, {Component} from "react";
import DatePicker from "react-datepicker";
import {addElement} from "../../helpers/DynamicElementsHelper";
import moment from "moment";
const uuid = require('uuid/v1');
require('react-datepicker/dist/react-datepicker.css');
class Fixtures extends Component {
constructor() {
super();
Fixtures.addMatch = Fixtures.addMatch.bind(this);
this.state = {
startDate: moment()
};
}
handleChange(date) {
this.setState({
startDate: date
});
}
static addMatch() {
let html = "<div className=\"form-group\"><label className=\"control-label\">Add Match</label>" +
"<DatePicker selected={this.state.startDate} onChange={this.handleChange.bind(this)} /></div>";
addElement('fixture-parent', 'newMatch', uuid(), html);
}
render() {
return (
<div className="tray tray-center">
<div className="row">
<div className="col-md-8">
<div className="panel mb25 mt5">
<div className="panel-heading">
<span className="panel-title">Fixtures</span>
<p>A list of fixtures currently on the system, pulled in via ajax from Ratpack</p>
</div>
<div className="panel-body p20 pb10">
<div id="fixture-parent" className="form-horizontal">
<div className="form-group">
<label className="control-label">Add Match</label>
<DatePicker
selected={this.state.startDate}
onChange={this.handleChange.bind(this)}/>
</div>
</div>
</div>
<button onClick={Fixtures.addMatch }>Add Match</button>
</div>
</div>
</div>
</div>
);
}
}
export default Fixtures;
In React, it is recommended to not interact with the DOM directly. Instead, you should modify the JSX depending on data that you have. For your code, instead of adding an HTML tag with data from the state that changes, you should change the state and display information based on that:
addMatch() {
//Add a start date to the list of starting dates
this.setState({
listOfStartDates: [...this.state.listOfStartDates, newDate]
});
}
render(){
//For each starting date, generate a new 'match' as you called them
// note: this is the same as the stringyfied HTML tag OP had in the question
let listOfMatches = this.state.listOfStartDates.map((date)=>{
return (
<div className="form-group">
<label className="control-label">
Add Match
</label>
<DatePicker selected={date} onChange={this.handleChange.bind(this)} />
</div>
);
/* then in here your returned JSX would be just as OP originally had, with the exception that you would have {listOfMatches} where OP used to have the inserted list of HTML tags */
return //...
}
Since the component will re-render every time the state changes, the component will always have as many matches as you have starting dates.
Hope that helps!
Put a space between DatePicker and selected.
"<DatePicker selected={this.state.startDate}onChange={this.handleChange.bind(this)}/></div>";

Vue.JS 2.0 slow when modifying unrelated data

Suppose I have an input field in Vue.JS that v-model bind to a String data property, and a long list of random numbers that are completely unrelated to that first String.
data: {
input: "",
randoms: []
}
<input type="text" v-model="input">
<p v-for="random in randoms" v-text="random"></p>
When I put both in the same Vue, I see a huge slowdown when typing in the input field, as it appears Vue is reevaluating the DOM for each list entry after every input event, although they really have nothing to do with each other.
https://jsfiddle.net/5jf3fmb8/2/
When I however move the v-for to a child component where I bind randoms to a prop, I experience no such slowdown
https://jsfiddle.net/j601cja8/1/
Is there a way I can achieve the performance of the second fiddle without using a child-component?
Is there a way I can achieve the performance of the second fiddle without using a child-component?
Short answer
No.
Long answer
Whenever any dependency of the template changes, Vue has to re-run the render function for the entire component and diff the new virtualDOM against the new one. It can't do this for this or that part of the template only, and skip the rest. Therefore, each time the input value changes, the entire virutalDOM is re-rendered.
Since your v-for is producing quite a bit of elements, this can take a few 100ms, enough to be noticable when you type.
Extracting the heavy part of the template into its own component is in fact the "official" way to optimize that.
As Alex explained, v-model.lazy might improve the situation a bit, but does not fix the core of the issue.
Shortest, simplest answer: change v-model to v-model.lazy.
When I put both in the same Vue, I see a huge slowdown when typing in the input field, as it appears Vue is reevaluating the DOM for each list entry after every input event, although they really have nothing to do with each other.
Note that the OnceFor sample still chugs like mad despite not actually being reactive any more. I don't understand Vue well enough to say if that's intentional or not.
const Example = {
data() { return { input: "", randoms: [] } },
created() { this.newRandoms() },
methods: {
newRandoms() { this.randoms = Array(50000).fill().map(() => Math.random()) }
}
}
new Vue({
el: "#vue-root",
data(){ return {example: 'lazy-model'}},
components: {
LazyModel: {...Example, template: "#lazy-model"
},
OnceFor: {...Example, template: "#once-for"
},
InlineTemplate: {...Example, template: "#inline-template",
components: {
Welp: {
props: ['randoms']
}
}
}
}
})
button,
input,
div {
margin: 2px;
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="vue-root">
<span><button v-for="(component, name) in $options.components" #click="$set($data, 'example', name)">{{name}}</button></span>
<component :is="example"></component>
</div>
<template id="lazy-model">
<div>
<input type="text" v-model.lazy="input"><br>
<input type="submit" value="Regenerate" #click="newRandoms">
<p v-for="random of randoms" v-text="random"></p>
</div>
</template>
<template id="once-for">
<div>
<input type="text" v-model="input"><br>
<input type="submit" value="Regenerate" #click="newRandoms">
<p v-for="random of randoms" v-text="random" v-once></p>
</div>
</template>
<template id="inline-template">
<div>
<input type="text" v-model="input"><br>
<input type="submit" value="Regenerate" #click="newRandoms">
<welp :randoms="randoms" inline-template>
<div>
<p v-for="(random, index) of randoms" :key="index"> {{index}}: {{random}} </p>
</div>
</welp>
</div>
</template>

What is workflow of the React

The code below is from React, which updates the DOM dynamically. I used the tutorial by Facebook react but did not understand the whole code, i.e which part of the code executes when and how it triggers the rest of the parts in the code. Please kindly help me in understanding the code.
var TodoList = React.createClass({
render: function() {
var createItem = function(itemText) {
return <li>{itemText}</li>;
};
return <ul>{this.props.items.map(createItem)}</ul>;
}
});
var TodoApp = React.createClass({
getInitialState: function() {
return {items: [], text: ''};
},
onChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var nextItems = this.state.items.concat([this.state.text]);
var nextText = '';
this.setState({items: nextItems, text: nextText});
},
render: function() {
return (
<div>
<h3>TODO</h3>
<TodoList items={this.state.items} />
<form onSubmit={this.handleSubmit}>
<input onChange={this.onChange} value={this.state.text} />
<button>{'Add #' + (this.state.items.length + 1)}</button>
</form>
</div>
);
}
});
React.renderComponent(<TodoApp />, mountNode);
The above code is used to dynamically update the DOM structure. This code is referred from http://facebook.github.io/react/ so please help in knowing the work process of the code.
Thanks, that's a very good question. Here's a rough overview of what is happening behind the scenes:
Initialization
It all starts with this line:
React.renderComponent(<TodoApp />, mountNode);
This instantiate the TodoApp component which calls:
TodoApp::getInitialState()
then, it renders the TodoApp component
TodoApp::render()
which in turns instantiate a TodoList
TodoList::render()
At this point, we have everything we need in order to render the initial markup
<div>
<h3>TODO</h3>
<ul></ul> <!-- <TodoList> -->
<form>
<input value="" />
<button>Add #1</button>
</form>
</div>
It is stringified and added inside of mountNode via innerHTML
OnChange
Then let's say you're going to enter some text in the input, then
TodoApp::onChange
is going to be called, which is going to call
TodoApp::setState
and in turn will call
TodoApp::render
again and generate the updated DOM
<div>
<h3>TODO</h3>
<ul></ul> <!-- <TodoList> -->
<form>
<input value="sometext" />
<button>Add #1</button>
</form>
</div>
What's happening at this point is that React is going to do a diff between the previous DOM and the current one.
<div>
<input
- value=""
+ value="sometext"
Only the value of the input changed, so React is going to just update this particular attribute in the real DOM.
You can find more general explanation on React official page.
Generally the react lifecycle can be described by the following stages (which can repeat multiple times once the components is created):
Initializing values (only once):
constructor(){ ... }
Mounting, if you need to add something after initial rendering (only once):
componentDidMount(){...}
Re-rendering functions, variables and components
myArrowFunction = () => {
...
this.setState({...})
...
}
Updating:
componentDidUpdate()}{...}
shouldComponentUpdate(){...}
Unmounting:
componentWillUnmount(){...}
Rendering happens here
render(){...}

Categories

Resources