I'm trying to work with a checkbox tree component like this: https://www.npmjs.com/package/react-checkbox-tree, except I'm storing the items that I have selected in Redux. Moreover, the only items that I'm actually storing are the leaf nodes in the tree. So for example, I'd have the full options data which would be used to render the tree:
const fam = {
cuz2: {
name: 'cuz2',
children: {
cuzKid2: {
name: 'cuzKid2',
children: {
}
}
}
},
grandpa: {
name: 'grandpa',
children: {
dad: {
name: 'dad',
children: {
me: {
name: 'me',
children: {}
},
sis: {
name: 'sis',
children: {}
}
}
},
aunt: {
name: 'aunt',
children: {
cuz: {
name: 'cuz',
children: {
name: 'cuzkid',
children: {}
}
}
}
}
}
}
and a separate object that stores the items selected. The following would be the only items that would appear if every checkbox was checked:
const selected = {
cuz2: true,
me: true,
sis: true,
cuz: true
}
I seem to be struggling with this method for having the UI determine which boxes to have fully, partially, or un-checked based on the selected object. I was wondering if anyone can recommend another strategy of accomplishing this.
So I have used react-checkbox-tree but I have customised a bit the icons in order to use another icons library.
Check my example on sandbox:
The library provides a basic example of how to render a tree with selected and/or expanded nodes.
All you need to do is:
set up the nodes with a unique 'value'
Choose which items should be selected (it may comes from Redux)
pass nodes & checked list to the CheckBox constructor
also be sure that when user select/unselect, you update the UI properly using the state
Your code should look similar to this:
import React from 'react';
import CheckboxTree from 'react-checkbox-tree';
const nodes = [{
value: '/cuz2',
label: 'cuz2',
children: [],
},
// other nodes
];
class BasicExample extends React.Component {
state = {
checked: [
'/cuz2'
],
expanded: [
'/cuz2',
],
};
constructor(props) {
super(props);
this.onCheck = this.onCheck.bind(this);
this.onExpand = this.onExpand.bind(this);
}
onCheck(checked) {
this.setState({
checked
});
}
onExpand(expanded) {
this.setState({
expanded
});
}
render() {
const {
checked,
expanded
} = this.state;
return (<
CheckboxTree checked={
checked
}
expanded={
expanded
}
nodes={
nodes
}
onCheck={
this.onCheck
}
onExpand={
this.onExpand
}
/>
);
}
}
export default BasicExample;
Related
Long story short(maybe it is not so short after all): on the same page I want to import and load dynamic components based on the selected module.
I have an object defined in the assets which contains the informations about the components that should be loaded for each module, it looks like this:
export const modules = {
module1: {
calls: {...},
components: [
{
url: 'shared/PreviewItem',
properties: [
{
name: 'width',
value: 'leftComponentWidth'
}
]
},
{
url: 'shared/ResizingDivider',
properties: []
},
{
url: 'forms/FormItem',
properties: [
{
name: 'width',
value: 'rightComponentWidth'
},
{
name: 'item',
value: 'item'
}
]
}
]
},
module2: {...}
}
Then I have my index page:
<template>
<div class="item-content">
<component
:is="component"
v-for="(component, i) in dataComponents"
:key="i"
v-bind="component.propertiesToPass"
#emit-action="emitAction($event)"
/>
</div>
</template>
<script>
export default {
data() {
return {
item: null,
rightComponentWidth: 50,
leftComponentWidth: 50,
dataComponents: []
}
},
created() {
this.importComponents()
},
methods: {
importComponents() {
this.dataComponents = []
modules[this.$route.params.module].components.forEach(
(component) => {
import(`~/components/${component.url}`).then((res) => {
res.default.propertiesToPass = []
component.properties.forEach((prop) => {
res.default.propertiesToPass.push({
[prop.name]: this[prop.value]
})
})
this.dataComponents.push(res.default)
})
}
)
},
emitAction(event) {
this[event.function](event.props)
},
changeComponentsWidth(event) {
this.leftComponentWidth -= event
this.rightComponentWidth = 100 - this.leftComponentWidth
}
}
}
}
</script>
As it is probably easy to understand I have to components and one divider between them that can be dragged to the right or to the left for resize the width of the other two components.
The components are getting loaded and imported correctly, and the props are passed right, so the width of both of the components in the start are 50 50.
The issue is that by doing [prop.name]: this[prop.value] I am setting the props to the value of this[prop.value] variable, and not to the variable itself, so, when I try to resize the components by using the divider, the variables get updated but the props get not.
Then the props are not responsive or reactive, are fixed.
The only way to update the props of the components is to add the following lines to the changeComponentsWidth() method:
this.dataComponents[0].propertiesToPass[0].width = this.leftComponentWidth
this.dataComponents[2].propertiesToPass[0].width = this.rightComponentWidth
But this is not a very dynamic way.
So My question is:
Is it possible to bind the props to the variable itself instead of just passing its value?
Or are there other "dynamic" ways to keep my props "responsive and reactive"?
I'm using react-dropdown-tree-select , After the child nodes are selected one by one, the parent node is not automatically selected. Is there a solution?
import React from 'react'
import DropdownTreeSelect from 'react-dropdown-tree-select'
import 'react-dropdown-tree-select/dist/styles.css'
import './test.css'
const data = {
label: 'search me',
value: 'searchme',
children: [
{
label: 'search me too',
value: 'searchmetoo',
children: [
{
label: 'No one can get me',
value: 'anonymous',
},
],
},
],
}
export default function testRcTree() {
const onChange = (currentNode, selectedNodes) => {
console.log('onChange::', currentNode, selectedNodes)
}
const onAction = (node, action) => {
console.log('onAction::', action, node)
}
const onNodeToggle = currentNode => {
console.log('onNodeToggle::', currentNode)
}
return (
<div >
<DropdownTreeSelect
multiSelect
className='mdl-demo'
data={data}
onChange={onChange}
onAction={onAction}
onNodeToggle={onNodeToggle} />
</div>
)
}
You would need to explicitly mark the checked attribute of parent node as true. There are two variables for a node's parent, "checked" and "expanded" I see expanded is true. But please find the parent of that node,which is sitting at selectedNode.parent and set it as
selectedNode.parent.checked=true;
I am passing an array data from parent to child component and I have encountered the following situations:
parent.component.html:
<child-component
...
[options]="students"
>
</child-component>
Status I: When I set the array on definition, everything is ok and I can get the array values on the child component.
parent.component.ts:
export class ParentComponent implements OnInit {
students: any[] = [
{ name: "Mary" },
{ name: "Marta" },
{ name: "Kelly" },
{ name: "John" },
{ name: "Shelley" },
{ name: "Frankstein" },
{ name: "Shierley" },
{ name: "Igor" }
];
}
child.component.ts:
export class ChildComponent implements OnInit {
#Input() options: any[]= [];
}
Status II: However, when I set the array on a method instead of definition, I get the input value as null. Why I want to fill the array in a method is that I fill it by retrieving data from server. So, I struggled with this problem and finally found that the problem is not related to async data. It is related to this definition place. So, how can I perform the data can be passed by its array values?
parent.component.ts:
export class ParentComponent implements OnInit {
students: any[] = [];
ngOnInit() {
this.getStudents();
}
getStudents() {
this.students = [
{ name: "Mary" },
{ name: "Marta" },
{ name: "Kelly" },
{ name: "John" },
{ name: "Shelley" },
{ name: "Frankstein" },
{ name: "Shierley" },
{ name: "Igor" }
];
}
Note: I think assigning null to the students on defining it. But otherwise it throws error and I encounter null value exception on child. Maybe lifcel-ycle related problem, but I have already tried ngOnchanges, ngAfterViewInit, etc.
If you do not directly assign an object which is passed to a child component, initially the input will receive undefined or an empty array since you are assigning an empty array to students variable.
To avoid it use conditional rendering with ngIf:
<child-component
*ngIf="students && students.length > 0"
[options]="students"
>
</child-component>
I'm using this component here: https://react-select.com/home
What I'm trying to accomplish is on page load if my array contains a value in my Assigned element then I want to display that by default in my react-select box if not then I want my placeholder Assign to to show.
Here is how my select looks:
<Select
value={this.state.Product[0].Assigned ? this.state.Product[0].Assigned : selectedAssigned}
onChange={this.handleChangeAssigned}
options={this.state.AssignedList}
placeholder={<div>Assign to:</div>}
/>
In my Product[0].Assigned there is currently a value, however the dropdown still has the placeholder Assign To. I tried changing value to value={this.state.Product[0].Assigned} but still no luck.
Here is my change handle:
handleChangeAssigned = selectedAssigned => {
this.setState(
{ selectedAssigned },
() => console.log(`Option selected:`, this.state.selectedAssigned)
);
};
It seems like you are storing the same data in multiple places. You are checking if you have an assignment by looking at this.state.Product[0].Assigned. But when you select an assignment you are not updating that property. Instead you are updating a completely separate property this.state.selectedAssigned. this.state.Product[0].Assigned never changes so if you see a placeholder at first then you will always see a placeholder.
The value that you set on the Select needs to be the same as the value that you update.
import React from "react";
import Select from "react-select";
interface Option {
label: string;
value: string;
}
interface State {
Product: any[];
AssignedList: Option[];
selectedAssigned: Option | null;
}
export default class MyComponent extends React.Component<{}, State> {
state: State = {
Product: [],
AssignedList: [
{ label: "a", value: "a" },
{ label: "b", value: "b" },
{ label: "c", value: "c" }
],
selectedAssigned: null
};
handleChangeAssigned = (selectedAssigned: Option | null) => {
this.setState({ selectedAssigned }, () =>
console.log(`Option selected:`, this.state.selectedAssigned)
);
};
render() {
return (
<Select
value={this.state.selectedAssigned}
onChange={this.handleChangeAssigned}
options={this.state.AssignedList}
placeholder={<div>Assign to:</div>}
/>
);
}
}
Code Sandbox Link
When the page first loads, the delete buttons generated by the code below work as expected. However, if you alter the text in one of the <textarea> elements, the delete button no longer works correctly. How can I fix this?
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
list: { type: Array },
};
}
constructor() {
super();
this.list = [
{ id: "1", text: "hello" },
{ id: "2", text: "hi" },
{ id: "3", text: "cool" },
];
}
render() {
return html`${this.list.map(item =>
html`<textarea>${item.text}</textarea><button id="${item.id}" #click="${this.delete}">X</button>`
)}`;
}
delete(event) {
const id = event.target.id;
this.list = this.list.filter(item => item.id !== id);
}
}
customElements.define("my-element", MyElement);
I'm not sure of the exact cause, but I think it has to do with the way lit-html decides which DOM elements to remove when rendering a list with fewer items than the previous render. The solution is to use the repeat directive. It takes as its second argument a function that helps lit-html identify which DOM elements correspond to which items in the array:
import { repeat } from 'lit-html/directives/repeat.js'
// ...
render(){
return html`
${repeat(this.list, item => item.id,
item => html`<textarea>${item.text}</textarea><button id="${item.id}" #click="${this.delete}">X</button><br>`
)}
`;
}