I am trying to render HTML from JSON into my React component. I am aware of the dangerouslySetInnerHTML and am not looking to use that as I would like to leverage other React components inside of the rendered HTML. Currently I can render any element as long as I close the tag. But this is not the case as in if I would like to put multiple elements or img tags in a div i would need it to remain open until all of the img tags have completed. I would appreciate any help on this. Thanks
render(){
var data = [
{
type: 'div',
open: true,
id: 1,
className: 'col-md-12 test',
value: ''
},
{
type: 'div',
open: false,
id: 1
}
]
var elements = data.map(function(element){
if(element.open){
return <element.type className={element.className}>
} else {
return </element.type>
}
})
return (
<div>
{elements}
</div>
)
}
webpack error
3 | return <element.type className={element.className}>
34 | } else {
> 35 | return </element.type>
| ^
36 | }
37 | })
38 | return (
# ./src/index.js 9:11-38
The data structure is redundant; you can simply do this:
render() {
var elements = (
<div id='1' className='col-md-12 test'></div>
);
return (
<div>{elements}</div>
);
}
Element trees themselves are just data. Rather than trying to invent a data structure to represent one, just use JSX.
The closing tag wasn't proper.
check the following fiddle.
https://jsfiddle.net/pranesh_ravi/mLoL6pzz/
Hope it helps!
I think #pranesh answered right. It has to go something like this
var elements = data.map(function(element){
if(element.open){
return <element.type className ={element.className}>Sample</element.type>
} else {
return <element.type>Sample</element.type>
}
})
Related
I have a JSON list of items that I import in my vue component,
I loop though that file in order to show them. Each item belong to a specific 'group' :
See IMG
E.g. :
{
"type": "Simple list",
"title": "Simple list",
"id": 1,
"group": "list-component",
"properties": "lorem lipsum"
},
I would like to apply a CSS 'border-top-color' to each item according to its group.
I was trying to apply the conditions when mouted(){} but I'm not sure if I'm doing it right. Here's my atempt :
The template (I'm using VueDraggable, don't mind it) :
<div class="item drag" :key="element" :style="[{ 'border-top-color': 'brdrTpClr' }]">
{{ element.title }}
<div class="addico" :key="index">
<i class="fas fa-add"#click="$emit('pushNewElt', element.id)"></i>
</div>
</div>
The script :
data() {
return {
dragItems: dragItemsList,
brdrTpClr: "",
};
},
mounted() {
for (let i = 0; i <= 15; i++) {
if (this.dragItems[i].group == "list-component") {
// I'm not sure how to do it
// the color I want to apply : #00A3A1b
} else if (this.dragItems[i].group == "location-media-component") {
// #005EB8
} else if (this.dragItems[i].group == "container-component") {
// #0091DA
} else if (this.dragItems[i].group == "UI-component") {
// #6D2077
} else if (this.dragItems[i].group == "reader-scanner-component") {
// #470A68
}
}
},
I'm using i<=15 instead of i<=this.dragItems.length because of a bug, don't mind it too.
Probably the most efficient (performance wise) and the most readable solution would be to declare a constant colorMap, outside the component, and then return the correct value or a fallback, using a method:
<script>
const colorMap = {
"list-component": '#00A3A1',
"location-media-component": '#005EB8',
"container-component": '#0091DA',
"UI-component": '#6D2077',
"reader-scanner-component": '#470A68'
}
export default {
//...
methods: {
borderColor(group) {
return colorMap[group] || '#000'
}
}
}
</script>
<template>
...
<div :style="{borderColor: borderColor(element.group)}">
content...
</div>
</template>
As a general rule, you want to take anything more complicated than a simple ternary outside of the template and provide it via either computed or methods.
Side note: the above method can also be written as computed:
computed: {
borderColor: group => colorMap[group] || '#000'
}
If you find yourself needing the colorMap in more than one component, export it from a constants.(js|ts) file and import everywhere needed. I typically name that file helpers, as it typically also contains static functions or maps (anything I reuse across multiple components/modules).
Important: you're currently passing an array to :style. You should be passing an object.
I would make a method called getBorderColor(item) which returns the colour based on the group, and then dynamically bind it using Vue.
<div
class="item drag"
:style="[{ 'border-top-color': getBorderColor(element) }]"
>
{{ element.title }}
// Add icon etc.
</div>
getBorderColor(element) {
// Can use a switch statement, but here's a simple ternary if/else as an example
return element.group === "list-component" ? `#00A3A1b`
: element.group === "location-media-component" ? `#005EB8`
: element.group === "container-component" ? `#0091DA`
: element.group === "UI-component" ? `#6D2077`
: element.group === "reader-scanner-component" ? `#470A68`
: `#000000`; // Default colour
}
or for a cleaner option you can have an object with your groups as keys and colours as values in data e.g.
return {
dragItems: dragItemsList,
brdrTpClr: "",
colors: {
"list-component": `#00A3A1b`,
"location-media-component": `#005EB8`,
// etc.
},
};
getBorderColor(element) {
return this.colors[element.group] || `#000`;
}
I am having a h1 with v-for and i am writing out things from my array ,it looks like this:
<h1
v-for="(record, index) of filteredRecords"
:key="index"
:record="record"
:class="getActiveClass(record, index)"
>
<div :class="getClass(record)">
<strong v-show="record.path === 'leftoFront'"
>{{ record.description }}
</strong>
</div>
</h1>
as you can see i am bindig a class (getActiveClass(record,index) --> passing it my record and an index)
This is my getActiveClass method:
getActiveClass(record, index) {
this.showMe(record);
return {
"is-active": index == this.activeSpan
};
}
i am calling a function called showMe passing my record to that and thats where the problem begins
the showMe method is for my setInterval so basically what it does that i am having multiple objects in my array and it is setting up the interval so when the record.time for that one record is over then it switches to the next one. Looks like this:
showMe(record) {
console.log(record.time)
setInterval(record => {
if (this.activeSpan === this.filteredRecords.length - 1) {
this.activeSpan = 0;
} else {
this.activeSpan++;
}
}, record.time );
},
this activeSpan is making sure that the 'is-active' class (see above) is changing correctly.
Now my problem is that the record.time is not working correctly when i print it out it gives me for example if iam having two objects in my array it console logs me both of the times .
So it is not changing correctly to its record.time it is just changing very fastly, as time goes by it shows just a very fast looping through my records .
Why is that? how can i set it up correctly so that when i get one record its interval is going to be the record.time (what belongs to it) , and when a record changes it does again the same (listening to its record.time)
FOR EXAMPLE :
filteredRecords:[
{
description:"hey you",
time:12,
id:4,
},
{
description:"hola babe",
time:43,
id:1
},
]
it should display as first the "hey you" text ,it should be displayed for 12s, and after the it should display the "hola babe" for 43 s.
thanks
<template>
<h1 ...>{{ filteredRecords[index].description }}</h1>
</template>
<script>
{
data() {
return {
index: 0,
// ...
};
},
methods: {
iterate(i) {
if (this.filteredRecords[i]) {
this.index = i;
window.setTimeout(() => iterate(i + 1), this.filteredRecords[i].time * 1000);
}
},
},
mounted() {
this.iterate(0);
},
}
</script>
How about this? Without using v-for.
I am making a pie chart for my data. I am using Angular Chart (and subsequently, charts.js).
My data looks like this (vm being the controller):
vm.persons = [
{
name:'smith',
cart: [
{
id: 1,
category: 'food'
},
{
id: 2,
category: 'clothes'
}
]
},
{
name: 'adams',
cart: [
{
id: 3,
category: 'automobile'
},
{
id:1, category: 'food'
}
]
}
]
As such, my template looks like:
<div ng-repeat="person in vm.persons">
<div class="person-header">{{person.name}}</div>
<!-- chart goes here -->
<canvas class="chart chart-pie" chart-data="person.cart | chart : 'category' : 'data'" chart-labels="person.cart | chart : 'category' : 'labels'"></canvas>
<div class="person-data" ng-repeat="item in person.cart">
<div>{{item.category}}</div>
</div>
</div>
I decided to go with a filter for the chart as I thought that would be appropriate, DRY and reusable:
angular.module('myModule').filter('chartFilter', function() {
return function(input, datum, key) {
const copy = JSON.parse(JSON.stringify([...input.slice()])); // maybe a bit overboard on preventing mutation...
const entries = Object.entries(copy.reduce((o,n) => {o[n[datum]] = (o[n[datum]] || 0) + 1}, {}));
const out = {
labels: entries.map(entry => entry[0]);
data: entries.map(entry => entry[1]);
};
return out[key];
}
});
THIS WORKS, and the chart does show up, with the proper data. However per the console, it throws an infdig error every time. Per the docs, it's because I am returning a new array, which I am because it is almost a different set of data. Even if I get rid of copy (which is meant to be a separate object entirely) and use input directly (input.reduce(o,n), etc.) it still throws the error.
I tried also making it into a function (in the controller):
vm.chartBy = (input, datum, key) => {
const copy = JSON.parse(JSON.stringify([...input.slice()])); // maybe a bit overboard on preventing mutation...
const entries = Object.entries(copy.reduce((o,n) => {o[n[datum]] = (o[n[datum]] || 0) + 1}, {}));
const out = {
labels: entries.map(entry => entry[0]);
data: entries.map(entry => entry[1]);
};
return out[key];
};
and in the template:
<canvas class="chart chart-pie" chart-data="vm.chartBy(person.cart, 'category', 'data')" chart-labels="vm.chartBy(person.cart, 'category', 'labels')"></canvas>
However this is throwing an infdig error as well.
Does anyone know how to not get it to through an infdig error each time? That is what I am trying to solve.
As you pointed out, you can't bind to a function which produces a new array or the digest cycle will never be satisfied that the new value matches the old, because the array reference changes each time.
Instead, bind only to the data and then implement the filter in the directive, so that the filtered data is never bound, just shown in the directive's template.
HTML
<canvas class="chart chart-pie" chart-data="person.cart" chart-labels="person.cart"></canvas>
JavaScript
app.directive('chartData', function(){
return {
template: '{{chartData | chart : "category" : "data"}}',
scope: {
'chartData': '='
}
}
});
app.directive('chartLabels', function(){
return {
template: '{{chartLabels | chart : "category" : "labels"}}',
scope: {
'chartLabels': '='
}
}
});
app.filter('chart', function() {
return function(input, datum, key) {
...
return out[key];
}
});
I've hardcoded the datum/key strings in the directives but you could pass those in as additional bindings if needed.
Simple Mock-up Fiddle
I'm building a key-command resource and giving VueJS a whirl while doing so. I'm a newbie but am gaining the grasp of things (slowly...).
I want to be able to search in a global search form for key commands I'm defining as actions within sections of commands (see data example below). I would like to search through all the actions to show only those that match the search criteria.
My HTML is below:
<div id="commands">
<input v-model="searchQuery" />
<div class="commands-section" v-for="item in sectionsSearched"
:key="item.id">
<h3>{{ item.section }}</h3>
<div class="commands-row" v-for="command in item.command" :key="command.action">
{{ command.action }}
</div>
</div>
</div>
My main Vue instance looks like this:
import Vue from 'vue/dist/vue.esm'
import { commands } from './data.js'
document.addEventListener('DOMContentLoaded', () => {
const element = document.getElementById("commands")
if (element != null) {
const app = new Vue({
el: element,
data: {
searchQuery: '',
commands: commands
},
computed: {
sectionsSearched() {
var self = this;
return this.commands.filter((c) => {
return c.command.filter((item) => {
console.log(item.action)
return item.action.indexOf(self.searchQuery) > -1;
});
});
},
}
});
}
});
And finally the data structure in data.js
const commands = [
{
section: "first section",
command: [
{ action: '1' },
{ action: '2' },
{ action: '3' },
],
},
{
section: "second section",
command: [
{ action: 'A' },
{ action: 'B' },
{ action: 'C' },
]
},
]
export { commands };
I'm able to output the commands using the console.log(item.action) snippet you see in the computed method called sectionsSearched.
I see no errors in the browser and the data renders correctly.
I cannot however filter by searching in real-time. I'm nearly positive it's a combination of my data structure + the computed method. Can anyone shed some insight as to what I'm doing wrong here?
I'd ideally like to keep the data as is because it's important to be sectioned off.
I'm a Rails guy who is new to this stuff so any and all feedback is welcome.
Thanks!
EDIT
I've tried the proposed solutions below but keep getting undefined in any query I pass. The functionality seems to work in most cases for something like this:
sectionsSearched() {
return this.commands.filter((c) => {
return c.command.filter((item) => {
return item.action.indexOf(this.searchQuery) > -1;
}).length > 0;
});
},
But alas nothing actually comes back. I'm scratching my head hard.
There is a issue in your sectionsSearched as it is returning the array of just commands.
See this one
sectionsSearched() {
return this.commands.reduce((r, e) => {
const command = e.command.filter(item => item.action.indexOf(this.searchQuery) > -1);
const section = e.section;
r.push({
section,
command
});
}, []);
}
const commands = [
{
section: "first section",
command: [
{ action: '1' },
{ action: '2' },
{ action: '3' },
],
},
{
section: "second section",
command: [
{ action: 'A' },
{ action: 'B' },
{ action: 'C' },
]
},
]
const element = document.getElementById("commands")
if (element != null) {
const app = new Vue({
el: element,
data: {
searchQuery: '',
commands: commands
},
computed: {
sectionsSearched() {
var self = this;
return this.commands.filter((c) => {
// the code below return an array, not a boolean
// make this.commands.filter() not work
// return c.command.filter((item) => {
// return item.action.indexOf(self.searchQuery) > -1;
// });
// to find whether there has command action equal to searchQuery
return c.command.find(item => item.action === self.searchQuery);
});
},
}
});
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="commands">
<input v-model="searchQuery" />
<div class="commands-section" v-for="item in sectionsSearched"
:key="item.id">
<h3>{{ item.section }}</h3>
<div class="commands-row" v-for="command in item.command" :key="command.action">
{{ command.action }}
</div>
</div>
</div>
Is that work as you wish ?
sectionsSearched() {
return this.commands.filter((c) => {
return c.command.filter((item) => {
return item.action.indexOf(this.searchQuery) > -1;
}).length > 0;
});
},
}
since filter will always return an array(empty or not) which value always is true.
Here is the problematic component in question.
const UserList = React.createClass({
render: function(){
let theList;
if(this.props.data){
theList=this.props.data.map(function(user, pos){
return (
<div className="row user">
<div className="col-xs-1">{pos}</div>
<div className="col-xs-5">{user.username}</div>
<div className="col-xs-3">{user.recent}</div>
<div className="col-xs-3">{user.alltime}</div>
</div>
);
}, this);
} else {
theList = <div>I don't know anymore</div>;
}
console.log(theList);
return (
theList
);
}
});
Whenever I attempt to return {theList}, I receive a Cannot read property '__reactInternalInstance$mincana79xce0t6kk1s5g66r' of null error. However, if I replace {theList} with static html, console.log prints out the correct array of objects that i want. As per the answers, I have tried to return both {theList} and theList but that didn't help.
In both cases, console.log first prints out [] which I assume is because componentDidMount contains my ajax call to get json from the server and has not fired yet before the first render(). I have tried to check against
this.props.data being null but it does not help.
Here is the parent component if it helps:
const Leaderboard = React.createClass({
getInitialState: function(){
return ({data: [], mode: 0});
},
componentDidMount: function(){
$.ajax({
url: 'https://someurlthatreturnsjson',
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error('https://someurlthatreturnsjson', status, err.toString());
}.bind(this)
});
},
render: function(){
return (
<div className="leaderboard">
<div className="row titleBar">
<img src="http://someimage.jpg"></img>Leaderboard
</div>
<HeaderBar />
<UserList data={this.state.data}/>
</div>
);
}
});
Ah OK, there were some interesting problems in here, but you were so close. The big one, with react you must always return a single top-level element (e.g. a div). So, your variable theList was actually an array of divs. You can't return that directly. But you can return it if it's wrapped in a single parent div.
const mockData = [
{
username: 'bob',
recent: 'seven',
alltime: 123,
},
{
username: 'sally mae',
recent: 'seven',
alltime: 133999,
},
];
var $ = {
ajax(opt) {
setTimeout(() => {
opt.success(mockData);
}, 200);
}
}
const UserList = React.createClass({
render: function(){
let theList;
if (this.props.data && this.props.data.length) {
theList = this.props.data.map(function(user, pos){
return (
<div key={user.username} className="row user">
<div className="col">{pos}</div>
<div className="col">{user.username}</div>
<div className="col">{user.recent}</div>
<div className="col">{user.alltime}</div>
</div>
);
});
} else {
theList = <div>There is NO data</div>;
}
return <div>{theList}</div>;
}
});
const Leaderboard = React.createClass({
getInitialState: function(){
return ({data: [], mode: 0});
},
componentDidMount: function(){
$.ajax({
url: 'https://someurlthatreturnsjson',
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error('https://someurlthatreturnsjson', status, err.toString());
}.bind(this)
});
},
render: function(){
return (
<div className="leaderboard">
<UserList data={this.state.data}/>
</div>
);
}
});
ReactDOM.render(
<Leaderboard/>,
document.getElementById('container')
);
.col {
width: 200px;
float: left;
}
<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>
<script src="https://facebook.github.io/react/js/jsfiddle-integration-babel.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
To explain the fiddle a little bit. Don't worry about the weird looking var $ stuff, I'm just stubbing out jQuery's ajax method so I can return some fake data after 200ms.
Also, for me jsfiddle gives me a 'bad config' message when I run it, but I close the message and the result is there. Don't know what that's about.
return (
{theList}
)
should just be
return theList
because you are not inside JSX at that point. What you're doing there will be interpreted as
return {
theList: theList
}
That's ES6 shorthand properties syntax.
Error can also arise from accessing nested state that doesn't exist:
I lack the reputation to comment, so adding an answer for future assistance -- I ran into this same issue for a different reason. Apparently, the error is triggered from an earlier error throwing off react's internal state, but the error is getting caught somehow. github issue #8091
In my case, I was trying access a property of state that didn't exist after moving the property to redux store:
// original state
state: {
files: [],
user: {},
}
// ... within render function:
<h1> Welcome {this.state.user.username} </h1>
I subsequently moved user to redux store and deleted line from state
// modified state
state: {
files: [],
}
// ... within render function (forgot to modify):
<h1> Welcome {this.state.user.username} </h1>
And this threw the cryptic error. Everything was cleared up by modifying render to call on this.props.user.username.
There is a small problem with the if statement:
if(this.props.data !== []){
should be:
if(this.props.data){
this.props.data is null, if the ajax call returns null. alternatively the code could be more elaborate.
const data = this.props.data;
if(data && data.constructor === Array && data.length > 0) {
Not sure if this is how you want to do it, but it works for me.
edit:
const UserList = React.createClass({
render: function() {
if(this.props.data){
return this.props.data.map(function(user, pos){
return (
<li> className="row user">
<span>{pos}</span>
<span>{user.username}</span>
<span>{user.recent}</span>
<span>{user.alltime}</span>
</li>
);
});
} else {
return <li>I don't know anymore</li>;
}
}
});
I encountered this error when I rendered a stateless component and decided to remove the react-root-element (wrapping div) after rendering it with basic dom manipulation.
Conclusion: be aware of this, better don't do it.