I am learning React-Redux and I have an issue navigating through the objects in my JSON file. I have the following file JSON, the file is designed to render out a side navigation:
export default function(){
return [
{
catId:"parMenu1",
parentCat:"Genres",
subcat:[
{
genre:"8Bit",
genreId:"1"
},
{
genre:"Acid House",
genreId:"2"
}
]
},
{
catId:"parMenu2",
parentCat:"sounds",
subcat:[
{
genre:"blah",
genreId:"3"
},
{
genre:"blah House",
genreId:"4"
}
]
]
}
I have the JSON file mapped to state props for a component. The component looks like so:
class BrowseByCont extends Component {
render () {
return (
<div className="browseByContInner">
{
console.log(this.props.reducerSidenav[0].catId)
}
</div>
)
}
}
function mapStateToProps(state) {
return {
reducerSidenav:state.reducerSidenav
};
}
I am trying to reach the subcats object within the parent object. The JSON object is linked to a variable called "reducerSidenav". So far I have managed to get this far into my JSON file: this.props.reducerSidenav[0].catId. this spits out the value parMenu1 which is the "parent" object I want to target. Where I am stuck though is I am trying to achieve two things:
firstly - I would like to access the first "parent" object by without having to refer to the first item in the array: reducerSidenav[0] but rather by find the catId with a value of parMenu1. This is because this list will be dynamic in future and referring to the first array object is not reliable.
secondy - I would then like to access the subcat object and get to the value thats associated to the key genre ie to return the value "8Bit"
You can use .find() to achieve both things.
class BrowseByCont extends React.Component {
render () {
let first = this.props.reducerSidenav.find(item => item.catId === "parMenu1");
let second = first.subcat.find(subcat => subcat.genre === "8Bit")
return (
<div className="browseByContInner">
<div>{first.catId}</div>
<div>{second.genre}</div>
</div>
)
}
}
Edit
In order to print all the subcats, you have to use .map()
class BrowseByCont extends React.Component {
render () {
let first = this.props.reducerSidenav.find(item => item.catId === "parMenu1");
return (
<div className="browseByContInner">
{first.subcat.map(genreItem =>
<div key={genreItem.genreId}>{genreItem.genre}</div>)
}
</div>
)
}
}
jsfiddle
Related
I have a following use case: I have a form and when submitting I want to add another (new) Vue 3 component as an item (li) into the unordered list (ul). This works for me when doing that this way:
data() {
return {
some_list: []
}
},
methods: {
doSomething(){
this.some_list.push(
MyComponent
);
}
}
BUT, I need to pass some arguments to that component as well. My component has got some properties defined. I just do not know HOW to pass those arguments. Just for information: I am using Vue 3 with Javascript, not Typescript.
I think the v-bind directive would help with that
data() {
return {
some_list: []
}
},
methods: {
doSomething(){
this.some_list.push(
{component:MyComponent, props:{...theprops}}
);
}
}
<v-for="{component, props} in some_list">
<component :is="component" v-bind="props"></component>
</v-for>
references
https://vuejs.org/guide/essentials/component-basics.html#dynamic-components
https://vuejs.org/api/built-in-directives.html#v-bind
I'm new to programming in Svelte. I would like to be able to use a method on an ES6 class instance in order to dynamically change values being used on my SPA. (Using svelte-spa-router is not an option, unfortunately.)
To get to the crux of the problem in a simplified from:
This is router.js:
import { writable } from 'svelte/store';
export class Router {
constructor(pageMap) {
this.pageMap = pageMap;
this.current = {};
this.currentName = '';
}
get name() {
return this.currentName;
}
set name(pageName) {
this.current = this.pageMap[pageName];
this.currentName = this.current.name;
}
navigate(target) {
this.name = target;
console.log(this.currentName);
}
}
and this is App.js:
<script>
import { Router } from './router';
const pageMap = {
start: {
title: 'start',
name: 'world!',
},
end: {
title: 'end',
name: '-- it works!!!',
},
}
const page = new Router(pageMap);
page.name = 'start';
</script>
<h1>Hello {page.currentName}</h1>
<button on:click={() => page.navigate('end')}>
change to "it works"
</button>
<button on:click={() => page.navigate('start')}>
change back to "world!"
</button>
The desired behavior is that the page.currentName value changes with the button presses. The output to the console on button presses is correct: "--it works!!!", or "world!". However, the text remains "Hello world!", so the value change is not traveling outside the class instance. If I had some way of saying "this = this" upon invoking the navigate method, that would probably solve the problem...
I suspect the correct answer involves writable stores, but I haven't quite been able to figure it out.
I suspect the correct answer involves writable stores
That is correct and trying to use classes like this is not helpful, at least with how Svelte operates right now.
Stores have to be declared at the top level of a component to be usable with $ syntax, putting them inside properties of classes and hiding them behind getters or setters just gets in the way.
I would just use a function that returns an object containing the stores and API you actually need, which then can be destructured right away and used in the markup. E.g.
import { writable, derived } from 'svelte/store';
export function router(pageMap) {
const current = writable({});
const currentName = derived(current, $current => $current.name ?? '');
function navigate(target) {
current.set(pageMap[target]);
}
return {
navigate,
currentName,
};
}
<script>
import { router } from './router';
const pageMap = {
start: {
title: 'start',
name: 'world!',
},
end: {
title: 'end',
name: '-- it works!!!',
},
}
const { navigate, currentName } = router(pageMap);
navigate('start');
</script>
<h1>Hello {$currentName}</h1>
<button on:click={() => navigate('end')}>
change to "it works"
</button>
<button on:click={() => navigate('start')}>
change back to "world!"
</button>
REPL example
You can do something similar with a class, but if you destructure it, the this binding will be broken, so all functions have to be bound manually or you have to pull out the store on its own and keep accessing the functions via the instance.
REPL example
Using React with TypeScript, there are several ways to define the type of children, like setting it to JSX.Element or React.ReactChild or extending PropsWithChildren. But doing so, is it possible to further limit which particular element that React child can be?
function ListItem() {
return (
<li>A list item<li>
);
}
//--------------------
interface ListProps {
children: React.ReactChild | React.ReactChild[]
}
function List(props: ListProps) {
return (
<ul>
{props.children} // what if I only want to allow elements of type ListItem here?
</ul>
);
}
Given the above scenario, can List be set up in such a way that it only allows children of type ListItem? Something akin to the following (invalid) code:
interface ListProps {
children: React.ReactChild<ListItem> | React.ReactChild<ListItem>[]
}
You can't constrain react children like this.
Any react functional component is just a function that has a specific props type and returns JSX.Element. This means that if you render the component before you pass it a child, then react has no idea what generated that JSX at all, and just passes it along.
And problem is that you render the component with the <MyComponent> syntax. So after that point, it's just a generic tree of JSX nodes.
This sounds a little like an XY problem, however. Typically if you need this, there's a better way to design your api.
Instead, you could make and items prop on List which takes an array of objects that will get passed as props to ListItem inside the List component.
For example:
function ListItem({ children }: { children: React.ReactNode }) {
return (
<li>{children}</li>
);
}
function List(props: { items: string[] }) {
return (
<ul>
{props.items.map((item) => <ListItem>{item}</ListItem> )}
</ul>
);
}
const good = <List items={['a', 'b', 'c']} />
In this example, you're just typing props, and List knows how to generate its own children.
Playground
Here's a barebones example that I am using for a "wizard" with multiple steps. It uses a primary component WizardSteps (plural) and a sub-component WizardStep (singular), which has a "label" property that is rendered in the main WizardSteps component. The key in making this work correctly is the Children.map(...) call, which ensures that React treats "children" as an array, and also allows Typescript and your IDE to work correctly.
const WizardSteps: FunctionComponent<WizardStepsProps> & WizardSubComponents = ({children}) => {
const steps = Children.map(children, child => child); /* Treat as array with requisite type */
return (
<div className="WizardSteps">
<header>
<!-- Note the use of step.props.label, which is properly typecast -->
{steps.map(step => <div className="WizardSteps__step">{step.props.label}</div>)}
</header>
<main>
<!-- Here you can render the body of each WizardStep child component -->
{steps.map(step => <div className="WizardSteps__body">{step}</div>)}
</main>
</div>
);
}
const Step: FunctionComponent<WizardStepProp> = ({label, onClick}) => {
return <span className="WizardSteps__label">
{label}
</span>
}
WizardSteps.Step = Step;
type WizardSubComponents = {
Step: FunctionComponent<WizardStepProp>
}
type WizardStepsProps = {
children: ReactElement<WizardStepProp> | Array<ReactElement<WizardStepProp>>
};
type WizardStepProp = {
label: string
onClick?: string
children?: ReactNode
}
Absolutely. You just need to use React.ReactElement for the proper generics.
interface ListItemProps {
text: string
}
interface ListProps {
children: React.ReactElement<ListItemProps> | React.ReactElement<ListItemProps>[];
}
Edit - I've created an example CodeSandbox for you:
https://codesandbox.io/s/hardcore-cannon-16kjo?file=/src/App.tsx
I am looking a way of determining the type object I clicked in JavaScript.
I populated in React (with PrimeReact - Tree View) a GUI element similar to a Tree View with a JSon object from a Rest call. Now the first layer of the Tree is from ObjectA and 2nd layer from ObjectB.
When user clicks, I don't want to raise events for ObjectA but only for ObjectB, so if I would do in Java, I would do something like following
ClickedObject.isAssignable(ObjectB)
which would tell me this is the type of the object I am interested.
I tried to do something like
ObjectB instanceof ClassB
but while I constructed the object from JSon, I don't have an access to its constructor (or is there a way to get the constructor of an object)..
What would be the best way to achieve what I want to do?
class App extends React.Component {
outer = (e) => {
console.log('outer');
}
inner = (e) => {
e.stopPropagation();
console.log('inner');
}
render() {
return (
<div onClick={this.outer}>
Outer
<div onClick={this.inner}>Inner</div>
</div>
)
}
};
Demo: https://codesandbox.io/s/j2j6x0lo6w
Or:
class App extends React.Component {
click = (e) => {
e.stopPropagation();
console.log(e.target.id);
}
render() {
return (
<div id="outer" onClick={this.click}>
Outer
<div id="inner" onClick={this.click}>Inner</div>
</div>
)
}
};
Or:
class App extends React.Component {
click(id, e) {
e.stopPropagation();
console.log(id);
}
render() {
return (
<div onClick={(e) => this.click('outer', e)}>
Outer
<div onClick={(e) => this.click('inner', e)}>Inner</div>
</div>
)
}
};
Edit
For the PrimeReact Tree component, you could do an object reference equality check. For example:
onSelectionChange(e) {
if (e.selection === data[1].children[2]) {
console.log('Clicked the 3rd child of the second parent');
}
}
If you take a look at Primereact tree node object, you'll notice that there is data property. You can use it to achieve what you want.
Fox example, populate list of nodes on the following way
var data = [
{
"label": "Documents",
"data": "ObjectA",
"expandedIcon": "fa-folder-open",
"collapsedIcon": "fa-folder",
"children": [{
"label": "Work",
"data": "ObjectB",
"expandedIcon": "fa-folder-open",
"collapsedIcon": "fa-folder"
},
{
"label": "Work",
"data": "ObjectB",
"expandedIcon": "fa-folder-open",
"collapsedIcon": "fa-folder"
}]
}];
(1st level nodes have data: "ObjectA", 2nd level nodes have data: "ObjectB",..)
Now you can check selected "object type" inside function onSelectionChange like this
onSelectionChange(e) {
console.log("Node selected");
console.log(e); //check out console to see structure of 'e'
if (e.selection.data == 'ObjectA'){
console.log("ObjectA selected");
} else if (e.selection.data == 'ObjectB'){
console.log("ObjectB selected");
} else {
console.log("Unknown object type selected");
}
}
Please note that you can assign any type to data property and build more complex data structure representing Primereact tree node.
I'm using Vue v1.0.28 and vue-resource to call my API and get the resource data. So I have a parent component, called Role, which has a child InputOptions. It has a foreach that iterates over the roles.
The big picture of all this is a list of items that can be selected, so the API can return items that are selected beforehand because the user saved/selected them time ago. The point is I can't fill selectedOptions of InputOptions. How could I get that information from parent component? Is that the way to do it, right?
I pasted here a chunk of my code, to try to show better picture of my problem:
role.vue
<template>
<div class="option-blocks">
<input-options
:options="roles"
:selected-options="selected"
:label-key-name.once="'name'"
:on-update="onUpdate"
v-ref:input-options
></input-options>
</div>
</template>
<script type="text/babel">
import InputOptions from 'components/input-options/default'
import Titles from 'steps/titles'
export default {
title: Titles.role,
components: { InputOptions },
methods: {
onUpdate(newSelectedOptions, oldSelectedOptions) {
this.selected = newSelectedOptions
}
},
data() {
return {
roles: [],
selected: [],
}
},
ready() {
this.$http.get('/ajax/roles').then((response) => {
this.roles = response.body
this.selected = this.roles.filter(role => role.checked)
})
}
}
</script>
InputOptions
<template>
<ul class="option-blocks centered">
<li class="option-block" :class="{ active: isSelected(option) }" v-for="option in options" #click="toggleSelect(option)">
<label>{{ option[labelKeyName] }}</label>
</li>
</ul>
</template>
<script type="text/babel">
import Props from 'components/input-options/mixins/props'
export default {
mixins: [ Props ],
computed: {
isSingleSelection() {
return 1 === this.max
}
},
methods: {
toggleSelect(option) {
//...
},
isSelected(option) {
return this.selectedOptions.includes(option)
}
},
data() {
return {}
},
ready() {
// I can't figure out how to do it
// I guess it's here where I need to get that information,
// resolved in a promise of the parent component
this.$watch('selectedOptions', this.onUpdate)
}
}
</script>
Props
export default {
props: {
options: {
required: true
},
labelKeyName: {
required: true
},
max: {},
min: {},
onUpdate: {
required: true
},
noneOptionLabel: {},
selectedOptions: {
type: Array
default: () => []
}
}
}
EDIT
I'm now getting this warning in the console:
[Vue warn]: Data field "selectedOptions" is already defined as a prop. To provide default value for a prop, use the "default" prop option; if you want to pass prop values to an instantiation call, use the "propsData" option. (found in component: <default-input-options>)
Are you using Vue.js version 2.0.3? If so, there is no ready function as specified in http://vuejs.org/api. You can do it in created hook of the component as follows:
// InputOptions component
// ...
data: function() {
return {
selectedOptions: []
}
},
created: function() {
this.$watch('selectedOptions', this.onUpdate)
}
In your InputOptions component, you have the following code:
this.$watch('selectedOptions', this.onUpdate)
But I am unable to see a onUpdate function defined in methods. Instead, it is defined in the parent component role. Can you insert a console.log("selectedOptions updated") to check if it is getting called as per your expectation? I think Vue.js expects methods to be present in the same component.
Alternatively in the above case, I think you are allowed to do this.$parent.onUpdate inside this.$watch(...) - something I have not tried but might work for you.
EDIT: some more thoughts
You may have few more issues - you are trying to observe an array - selectedOptions which is a risky strategy. Arrays don't change - they are like containers for list of objects. But the individual objects inside will change. Therefore your $watch might not trigger for selectedOptions.
Based on my experience with Vue.js till now, I have observed that array changes are registered when you add or delete an item, but not when you change a single object - something you need to verify on your own.
To work around this behaviour, you may have separate component (input-one-option) for each of your input options, in which it is easier to observe changes.
Finally, I found the bug. I wasn't binding the prop as kebab-case