I have started to get my hand-on reactjs. I have three components which are placed in separate files. mainPage.js list.js and tableDisplay.jsare the three different files. I have got few doubt after working with reactjs.
The output from mainPage.js is passed as inputs to the list.js, so here
mainPage.js is parent and list.js is the child(if wrong please correct
me).Likewise, list.js output is passed as input to the tableDisplay.js,my doubt here is, does list.js remain as child component or act as parent component for the list.js.
The output fetched in the list.js is a table from the DB. So Im trying to
display the same table in the tableDisplay.js page, for which Im storing the fetched data to be an object. But Im getting an error Objects are not valid as a React child (found: object with keys {Name, Emp.no, Designation, WorkOff}). If you meant to render a collection of children, use an array instead or wrap the object using createFragment(object) from the React add-ons. Check the render method of tableDispay. So what does this error mean and how to overcome this error ad display the table in the tableDisplay.js
list.js :
class list extends React.Component {
constructor(props) {
super(props);
this.state = {
content: false,
query : '' ,
};
this.clickEvent= this.clickEvent.bind(this);
}
onChange(e) {
this.setState({ query : e.target.value });
}
clickEvent(event) {
event.preventDefault();
this.setState({
content: true,
});
connectDataBase.query(this.state.query, (err, result) => {
var dataFetched = result;
this.setState({
htmlTable: dataFetched,
});
})
}
render() {
return (
<div>
<div id="listClass">
<label> SQL </label>
<br />
<textarea rows="4" cols="50" onChange={this.onChange.bind(this)} value={this.state.query}> Query </textarea>
</div>
<button onClick={this.clickEvent.bind(this)} > Run </button>
<div id="third" >
{this.state.content && <TableDisplay tableData={this.state.htmlTable}/>}
</div>
</div>
)
}
}
export default list;
tableDisplay.js:
class tableDisplay extends React.Component{
constructor(props) {
super(props);
this.state = {
};
}
render(){
return(
<div id="tableClass" >
<label> Result </label>
<br/>
{this.props.tableData.map((x,y)=>
<table key={y}>
{x}
</table>
)}
{this.props.tableData};
</div>
)
}
}
export default tableDisplay;
I think error is in this line:
{this.props.tableData};
tableData is an array (as you are using map on that), and we can't render any array/object directly inside JSX, remove that line it will work.
Note: You can convert the array to string by using join then you can print the data directly.
Use this:
{this.props.tableData.join(' ')};
I think there is a issue with table rendering also, inside table you are directly putting the {x}, but you need to use some td/tr to render data.
You need to define the htmlTable and content in state variable, initial value of content should be false and once you get the data update the content value to true.
Content of tableData is:
[{Name : 'Sam', Emp.No:'12809', Designation:'Engg.', WorkOff :'UK'}]
Use this to render table:
<table>
{tableData.map((obj,y) => {
return <tr key={obj['Emp.No']}>
{Object.keys(obj).map((x,y) => <td key={y}> {obj[x]} </td>)
</tr>
})}
</table>
Related
Here's how my code is structured: parent component shuffles through child components via v-if directives, one of the child components is using a state to define its data. Everything works except when I switch between the child components. When I get back, no data can be shown because the state has become null.
Parent component:
<template>
<div>
<Welcome v-if="view==0" />
<Courses v-if="view==1" /> //the component that I'm working on
<Platforms v-if="view==2" />
</div>
</template>
Courses component:
<template>
<div>Content</div>
</template>
<script>
export default {
name: 'Courses',
computed: {
...mapState([
'courses'
])
},
data () {
return {
courseList: [],
len: Number,
}
},
created () {
console.log("state.courses:")
console.log(this.courses)
this.courseList = this.courses
this.len = this.courses.length
},
}
</script>
Let say the default value for "view" is 1, when I load the page, the "Courses" component will be shown (complete with the data). If I click a button to change the value of "view" to 0, the "Welcome" component is shown. However, when I tried to go back to the "Courses" component, the courses component is rendered but is missing all the data.
Upon inspection (via console logging), I found that when the "Courses" component was initially rendered, the state was mapped correctly and I could use it, but if I changed the "view" to another value to render another component and then changed it back to the original value, the "Courses" component still renders but the state became undefined or null.
EDIT: Clarification.
Set courseList to a component name and use <component>
and set some enum
eg.
<template>
<component :is="this.viewState" />
</template>
<script>
export default {
// your stuff
data() {
return {
viewState: 'Welcome',
}
},
methods: {
getComponent(stateNum) {
const States = { '1': 'Welcome', '2': 'Courses', '3': 'Platforms' }
Object.freeze(States)
return States[stateNum]
}
},
created() {
// do your stuff here
const view = someTask() // someTask because I don't get it from where you're getting data
this.viewState = this.getComponent(view)
}
}
</script>
I don't actually understood correctly but here I gave some idea for approaching your problem.
I am struggling to figure out how to implement data down, actions up in a glimmer component hierarchy (using Ember Octane, v3.15).
I have a parent component with a list of items. When the user clicks on a button within the Parent component, I want to populate an Editor component with the data from the relevant item; when the user clicks "Save" within the Editor component, populate the changes back to the parent. Here's what happens instead:
How can I make the text box be populated with "Hello", and have changes persisted back to the list above when I click "Save"?
Code
{{!-- app/components/parent.hbs --}}
<ul>
{{#each this.models as |model|}}
<li>{{model.text}} <button {{on 'click' (fn this.edit model)}}>Edit</button></li>
{{/each}}
</ul>
<Editor #currentModel={{this.currentModel}} #save={{this.save}} />
// app/components/parent.js
import Component from '#glimmer/component';
export default class ParentComponent extends Component {
#tracked models = [
{ id: 1, text: 'Hello'},
{ id: 2, text: 'World'}
]
#tracked currentModel = null;
#action
edit(model) {
this.currentModel = model;
}
#action
save(model) {
// persist data
this.models = models.map( (m) => m.id == model.id ? model : m )
}
}
{{!-- app/components/editor.hbs --}}
{{#if #currentModel}}
<small>Editing ID: {{this.id}}</small>
{{/if}}
<Input #value={{this.text}} />
<button {{on 'click' this.save}}>Save</button>
// app/components/editor.hbs
import Component from '#glimmer/component';
import { tracked } from "#glimmer/tracking";
import { action } from "#ember/object";
export default class EditorComponent extends Component {
#tracked text;
#tracked id;
constructor() {
super(...arguments)
if (this.args.currentModel) {
this.text = this.args.currentModel.text;
this.id = this.args.currentModel.id;
}
}
#action
save() {
// persist the updated model back to the parent
this.args.save({ id: this.id, text: this.text })
}
}
Rationale/Problem
I decided to implement Editor as a stateful component, because that seemed like the most idiomatic way to get form data out of the <Input /> component. I set the initial state using args. Since this.currentModel is #tracked in ParentComponent and I would expect re-assignment of that property to update the #currentModel argument passed to Editor.
Indeed that seems to be the case, since clicking "Edit" next to one of the items in ParentComponent makes <small>Editing ID: {{this.id}}</small> appear. However, neither the value of the <Input /> element nor the id are populated.
I understand that this.text and this.id are not being updated because the constructor of EditorComponent is not being re-run when currentModel changes in the parent... but I'm stuck on what to do instead.
What I've tried
As I was trying to figure this out, I came across this example (code), which has pretty much the same interaction between BlogAuthorComponent (hbs) and BlogAuthorEditComponent (hbs, js). Their solution, as applied to my problem, would be to write EditorComponent like this:
{{!-- app/components/editor.hbs --}}
{{#if this.isEditing}}
<small>Editing ID: {{#currentModel.id}}</small>
<Input #value={{#currentModel.text}} />
<button {{on 'click' this.save}}>Save</button>
{{/if}}
// app/components/editor.hbs
import Component from '#glimmer/component';
import { tracked } from "#glimmer/tracking";
import { action } from "#ember/object";
export default class EditorComponent extends Component {
get isEditing() {
return !!this.args.currentModel
}
#action
save() {
// persist the updated model back to the parent
this.args.save({ id: this.id, text: this.text })
}
}
It works! But I don't like this solution, for a few reasons:
Modifying a property of something passed to the child component as an arg seems... spooky... I'm honestly not sure why it works at all (since while ParentComponent#models is #tracked, I wouldn't expect properties of POJOs within that array to be followed...)
This updates the text in ParentComponent as you type which, while neat, isn't what I want---I want the changes to be persisted only when the user clicks "Save" (which in this case does nothing)
In my real app, when the user is not "editing" an existing item, I'd like the form to be an "Add Item" form, where clicking the "Save" button adds a new item. I'm not sure how to do this without duplicating the form and/or doing some hairly logic as to what goes in <Input #value...
I also came across this question, but it seems to refer to an old version of glimmer.
Thank you for reading this far---I would appreciate any advice!
To track changes to currentModel in your editor component and set a default value, use the get accessor:
get model() {
return this.args.currentModel || { text: '', id: null };
}
And in your template do:
{{#if this.model.id}}
<small>
Editing ID:
{{this.model.id}}
</small>
{{/if}}
<Input #value={{this.model.text}} />
<button type="button" {{on "click" this.save}}>
Save
</button>
Be aware though that this will mutate currentModel in your parent component, which I guess is not what you want. To circumvent this, create a new object from the properties of the model you're editing.
Solution:
// editor/component.js
export default class EditorComponent extends Component {
get model() {
return this.args.currentModel;
}
#action
save() {
this.args.save(this.model);
}
}
In your parent component, create a new object from the passed model. Also, remember to reset currentModel in the save action. Now you can just check whether id is null or not in your parent component's save action, and if it is, just implement your save logic:
// parent/component.js
#tracked currentModel = {};
#action
edit(model) {
// create a new object
this.currentModel = { ...model };
}
#action
save(model) {
if (model.id) {
this.models = this.models.map((m) => (m.id == model.id ? model : m));
} else {
// save logic
}
this.currentModel = {};
}
I am dynamically rendering a list of elements. Depending on key-value pairs in those elements, I need to insert other elements in front of them.
I'd like to use <sup></sup> tags on those elements, but they are showing as plain text instead of superscript.
How can I use JSX in the state which is an array of strings and not have it come out as plain text?
The line in question is this one: allOptions.splice(index, 0, this.props.levelNames[j]);
The prop would be : [...., '1<sup>st</sup> Level',...]
But when rendered it just comes out as plain text.
import React, { Component } from 'react';
import './App.css';
export class Chosen extends Component {
render() {
let allOptions = this.props.chosenSpells.map((val, i) => this.props.selectMaker(val, i, 'chosen'));
let index;
let headings=this.props.levelNames;
for (let j=this.props.highestSpellLevel; j>0;j--) {
index = this.props.chosenSpells.findIndex(function findLevel (element) {return element.level==j});
console.log(index);
console.log(headings[j]);
if (index>=0){
allOptions.splice(index, 0, this.props.levelNames[j]);
}
}
return (
<div>
<b><span className="choose">Chosen (click to remove):</span></b><br/>
<div className="my-custom-select">
{allOptions}
</div>
</div>
);
}
}
export class Parent extends Component {
constructor(props) {
super(props);
this.state = {
levelNames: ['Cantrips', '1<sup>st</sup> Level', '2nd Level', '3rd Level']
};
In order to display HTML tags in React JSX, you need to pass the HTML string to dangerouslySetInnerHTML props. Not inside a JSX tag. Please check this official documentation about how to do it: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
So, instead doing this:
<div className="my-custom-select">
{allOptions}
</div>
You should doing this way:
<div className="my-custom-select" dangerouslySetInnerHTML={{__html: allOptions}}/>
It is due to security concern. Quoted from React documentation:
dangerouslySetInnerHTML is React’s replacement for using innerHTML in the browser DOM. In general, setting HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) attack. So, you can set HTML directly from React, but you have to type out dangerouslySetInnerHTML and pass an object with a __html key, to remind yourself that it’s dangerous.
If you insist to put the HTML string inside JSX tag instead passing to props, alternatively you can use an additional library from this NPM package: https://www.npmjs.com/package/react-html-parser . So, it will be something looks like this:
import React from 'react';
import ReactHtmlParser, { processNodes, convertNodeToElement, htmlparser2 } from 'react-html-parser';
class HtmlComponent extends React.Component {
render() {
........
return <div className="my-custom-select">{ ReactHtmlParser(allOptions) }</div>;
}
}
I have a list of instruments that should render a c-input with autosuggest window when the user types something. Also, I need an option for c-input to add or remove autosuggest component.
/* instrument component */
<template>
<c-input ref="input"
:values="inputValue"
:placeholder="placeholder"
#input="onInput"
#change="onChangeInput"
#reset="reset" />
<autosuggest
v-if="showSuggests"
:inputValue="inputValue"
:suggests="suggests"
#onSelectRic="selectRicFromList"
></autosuggest>
</div>
</template>
<script>
export default {
name: 'instrument',
data: () => ({
suggests: [],
inputValue: '',
}),
computed: {
showSuggests() {
return this.isNeedAutosuggest && this.showList;
},
showList() {
return this.$store.state.autosuggest.show;
},
isloading() {
return this.$store.state.instruments.showLoading;
},
defaultValue() {
if (this.instrument.name) {
return this.instrument.name;
}
return '';
},
},
[...]
};
</script>
This is a parent component:
<template>
<div>
<instrument v-for="(instrument, index) in instruments"
:key="instrument.name"
:instrument="instrument"
:placeholder="$t('change_instrument')"
:isNeedAutosuggest="true" /> <!--that flag should manage an autosuggest option-->
<instrument v-if="instruments.length < maxInstruments"
ref="newInstrument"
:isNeedAutosuggest="true" <!-- here too -->
:placeholder="$t('instrument-panel.ADD_INSTRUMENT')" />
</div>
</template>
The main issues are I have so many autosuggests in DOM as I have instruments. In other words, there is should be 1 autosuggest component when the option is true. Moving autosuggest to the parent level is not good because of flexibility and a lot of logically connected with c-input.
Have you any ideas to do it?
[UPDATE]
Here is how I've solve this;
I created an another component that wraps input and autosuggest components. If I need need an input with autosuggest I will use this one, either I will use a simple input.
/* wrapper.vue - inserted into the Instrument.vue*/
<template>
<span>
<fc-input ref="input"
:values="value"
:placeholder="placeholder"
:isloading="isloading"
#input="onInput"
#changeInput="$emit('change', $event)"
#resetInput="onResetInput" />
<fc-autosuggest
v-if="isSuggestsExist"
:suggests="suggests"
/>
</span>
</template>
You can do it if you create a function inside each instrument component, which will call the parent component and search the first component instrument to find autosuggest. Function will be like that:
name: 'instrument',
...
computed: {
autosuggestComponent () {
// this is a pseudo code
const parentChildrenComponents = this.$parent.children();
const firstChild = parentChildrenComponents[0];
const autosuggestEl = firstChild.$el.getElementsByTagName('autosuggest')[0];
// return Vue component
return autosuggestEl.__vue__;
}
},
methods: {
useAutosuggestComponent () {
this.autosuggestComponent.inputValue = this.inputValue;
this.autosuggestComponent.suggests = [{...}];
}
}
This solution is not so beautiful, but it allows to keep the logic inside the instrument component.
But my advice is create some parent component which will contain instrument components and I suggest to work with autosuggest through the parent. You can create autosuggest component in the parent and pass it to the children instruments. And if instrument doesn't receive a link to a autosuggest (in props), than it will create autosuggest inside itself. It will allow to use instrument for different conditions.
Let me know if I need to explain my idea carefully.
I have created a vue component for selecting photos. When the user clicks any photo the id of the photo will be assigned to a hidden input field inside the component.
Now I have used this component twice on the same page with different data. The problem is when I click on the photo of one component the value of the input field of both the components gets updated. I am using vue.js version 2.1.10
Here is the simplified version of my component.
<div>
<input type="hidden" :name="inputName" :value="currentSelectedPhoto.id">
<div v-for="photo in photos">
<div v-on:click="updateSelectedPhoto(photo)">
<img :src="photo.photo_url" />
</div>
</div>
</div>
The Component
export default {
props: {
...
},
methods: {
getPhotos(){
...
},
updateSelectedPhoto(photo){
this.currentSelectedPhoto = photo;
}
}
}
This is how I am using it in html
<div>
<div>
Profile Photo
<photo-selector
photos="{{ $photos }}"
input-name="profile_photo_id"
>
</photo-selector>
</div>
<div class="col-sm-4">
Cover Photo
<photo-selector
photos="{{ $otherPhotos }}"
input-name="cover_photo_id"
>
</photo-selector>
</div>
</div>
Based on your codepen sample, it's because you are sharing the state object between the two:
const initalState = {
selectedPhoto: null
};
const PhotoSelector = Vue.extend({
data: () => {
return initalState
},
Vue mutates the initial state object (by wrapping it in reactive getters etc), so you need to have data() return a fresh state object for the instance to use:
data: () => {
return {
selectedPhoto: null
};
},