I have a problem I can't solve on my own.
I have a JavaScript object with so-called GROUPS. Each group can have either one or more subgroups or also so-called systems.
My task now is to display this structure graphically - a kind of tree view (colored DIV elements).
But I don't have any idea how to read this object in order to build it up graphically.
My Object:
const test = {
grp: [
{
groupID: 'group-1',
grp : [
{
groupID: 'group-2',
grp: [
{
groupID: 'group-3',
sys: ['sys1','sys2','sys3']
},
{
groupID: 'group-4',
grp: [
{
groupID: 'group-5',
sys: ['sys4','sys5','sys6']
},
{
groupID: 'group-6',
grp: [
{
groupID: 'group-7',
sys: ['sys7','sys8','sys9']
}
]
}
]
}
],
sys: ['sys0']
}
]
}
]
};
Here is a graphical example:
https://pic-hoster.net/view/69453/grp-sys.jpg.htm
I very much hope that someone here can help me.
How would you approach this task?
Graphical representation (Tree-View) of a JavaScript object
The important thing here is to identify your data structure so that you can attack the problem recursively.
In your case your groups can be defined like this:
{
groupID: string,
sys?: string[],
grp?: IGroup[]
}
(Where IGroup defined the above structure)
All groups has a groupID, some have sys and some have grp children.
From here we can define a function where the logic is as the following:
Generate a two-row structure for the current group.
For each sys child element (if they exists), add a cell to the top row with the sys value.
For each grp child element (if they exists), call this function and append the returning structure to a cell in the top row.
Insert a cell into the bottom row at the child-element colspan width. Set cell content to the current grp groupID
Return the two-row structure element, either feeding into the recursive build process or as a final result.
Here is a rough implementation of the above points:
function groupToHTML(grp) {
//Table to append to and return
var container = document.createElement("table");
container.border = "1";
container.style.borderCollapse = "collapse";
//Insert row to children of this node
var childRow = container.appendChild(document.createElement("tr"));
//Append all "sys" elements as cells
if (grp.sys !== void 0) {
for (var sysIndex = 0; sysIndex < grp.sys.length; sysIndex++) {
var sys = grp.sys[sysIndex];
var sysCell = childRow.appendChild(document.createElement("td"));
sysCell.innerHTML = sys;
sysCell.style.backgroundColor = "red";
sysCell.style.verticalAlign = "bottom";
}
}
//Append all "grp" children by calling "groupToHTML" on them and appending the returning table
if (grp.grp !== void 0) {
for (var grpIndex = 0; grpIndex < grp.grp.length; grpIndex++) {
var child = grp.grp[grpIndex];
var grpCell = childRow.appendChild(document.createElement("td"));
grpCell.appendChild(groupToHTML(child));
grpCell.style.verticalAlign = "bottom";
}
}
//Add a row and cell for "this" grp
var thisRow = container.appendChild(document.createElement("tr"));
var thisCell = thisRow.appendChild(document.createElement("th"));
thisCell.innerHTML = grp.groupID;
thisCell.style.textAlign = "center";
//Set cell colspan to number of child elements
thisCell.colSpan = Math.max(1, (grp.grp !== void 0 ? grp.grp.length : 0) + (grp.sys !== void 0 ? grp.sys.length : 0));
//Return table
return container;
}
//TEST
//testdata
var data = {
groupID: 'group-1',
grp: [
{
groupID: 'group-2',
grp: [
{
groupID: 'group-3',
sys: ['sys1', 'sys2', 'sys3']
}, {
groupID: 'group-4',
grp: [
{
groupID: 'group-5',
sys: ['sys4', 'sys5', 'sys6']
}, {
groupID: 'group-6',
grp: [
{
groupID: 'group-7',
sys: ['sys7', 'sys8', 'sys9']
}
]
}
]
}
],
sys: ['sys0']
}
]
};
//Initiate
var node = groupToHTML(data);
//Append
document.body.appendChild(node);
You could use recursive function to create nested levels for each grp and its systems. So each level will have name and children elements. Children elements will be nested groups and systems.
Pure javascript solution
const test = {"grp":[{"groupID":"group-1","grp":[{"groupID":"group-2","grp":[{"groupID":"group-3","sys":["sys1","sys2","sys3"]},{"groupID":"group-4","grp":[{"groupID":"group-5","sys":["sys4","sys5","sys6"]},{"groupID":"group-6","grp":[{"groupID":"group-7","sys":["sys7","sys8","sys9"]}]}]}],"sys":["sys0"]}]}]}
function tree(data, parent) {
if(data.grp) {
data.grp.forEach(obj => {
const child = document.createElement('div')
child.className = 'child'
const children = document.createElement('div')
children.className = 'children'
if(obj.groupID) {
const name = document.createElement('div');
name.className = 'name'
name.textContent = obj.groupID
child.appendChild(name)
}
if(obj.sys) {
const system = document.createElement('div')
system.className = 'system';
obj.sys.forEach(s => {
const sys = document.createElement('div')
sys.className = 'item'
sys.textContent = s
system.appendChild(sys)
})
children.appendChild(system)
}
child.appendChild(children)
parent.appendChild(child)
tree(obj, children)
})
}
}
const root = document.body.querySelector('#root')
tree(test, root)
#root * {
color: white;
}
.system {
background: #E00022;
display: flex;
flex-direction: column-reverse;
padding: 10px;
}
.name {
background: #595959;
padding: 10px;
}
.child {
display: flex;
flex-direction: column-reverse;
}
.children {
display: flex;
align-items: flex-end;
}
.children > div {
flex: 1;
border-bottom: 1px solid white;
}
<div id="root"></div>
React solution
const {Component} = React;
const data = {"grp":[{"groupID":"group-1","grp":[{"groupID":"group-2","grp":[{"groupID":"group-3","sys":["sys1","sys2","sys3"]},{"groupID":"group-4","grp":[{"groupID":"group-5","sys":["sys4","sys5","sys6"]},{"groupID":"group-6","grp":[{"groupID":"group-7","sys":["sys7","sys8","sys9"]}]}]}],"sys":["sys0"]}]}]}
class Systems extends Component {
render() {
const { data } = this.props;
return <div className="systems">
{data.map((sys, i) => (
<div key={i} className="system">{sys}</div>
))}
</div>
}
}
class Group extends Component {
render() {
const { data } = this.props;
return data.map((group, i) => (
<div key={i} className="group">
{group.groupID && <div className="group-name">{group.groupID}</div>}
<div className="children">
{group.sys && <Systems data={group.sys} />}
{group.grp && <Group data={group.grp} />}
</div>
</div>
))
}
}
class App extends Component {
state = {
data: {}
}
componentWillMount = () => {
this.setState({ data: this.props.data })
}
render() {
console.log(this.state)
return <div className="root">
<Group data={this.state.data.grp} />
</div>
}
}
ReactDOM.render(
<App data={data} />,
document.getElementById('container')
);
#root * {
color: white;
}
.group {
display: flex;
flex-direction: column-reverse;
}
.group-name {
background: rgb(89, 89, 89);
padding: 10px;
color: white;
border-top: 1px solid white;
}
.group-name {
opacity: 0.85;
transition: all 0.25s;
}
.children {
display: flex;
}
.children > * {
flex: 1;
}
.systems {
display: flex;
flex-direction: column-reverse;
}
.system {
background: red;
color: white;
padding: 10px;
opacity: 0.6;
transition: all 0.25s;
border-top: 1px solid white;
}
.system:hover,
.group-name:hover{
opacity: 1;
}
.as-console-wrapper {
display: none !important;
}
<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="container"></div>
Related
In the following code, I have two columns. One column has all the tasks. The other column has a group of selected tasks. So, my expectation is that, when a user drags and drops a task from the total tasks column to the selected tasks column, the task should still be there in total tasks column. When the tasks are there in the selected tasks column, there will be a button visible to delete the tasks from selected tasks column. When I click this button, the task gets removed from the selected tasks column. But I am not able to drag it again from the total tasks column. I am getting the following error:
Unable to find draggable with id: task-2
Request you guys to take a look and tell me what's the mistake I am doing
Please find the link for the codesandbox:
https://codesandbox.io/s/unruffled-waterfall-wdyc5?file=/src/task.jsx
Also this is my code:
App.jsx:
import React from 'react';
import '#atlaskit/css-reset';
import styled from 'styled-components';
import { DragDropContext } from 'react-beautiful-dnd';
import initialData from './initial-data';
import Column from './column';
const Container = styled.div`
display: flex;
`;
class App extends React.Component {
state = initialData;
onDragStart = start => {
const homeIndex = this.state.columnOrder.indexOf(start.source.droppableId);
this.setState({
homeIndex,
});
};
onDragEnd = result => {
this.setState({
homeIndex: null,
});
const { destination, source, draggableId } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
const home = this.state.columns[source.droppableId];
const foreign = this.state.columns[destination.droppableId];
if (home === foreign) {
const newTaskIds = Array.from(home.taskIds);
newTaskIds.splice(source.index, 1);
newTaskIds.splice(destination.index, 0, draggableId);
const newHome = {
...home,
taskIds: newTaskIds,
};
const newState = {
...this.state,
columns: {
...this.state.columns,
[newHome.id]: newHome,
},
};
this.setState(newState);
return;
}
const foreignTaskIds = Array.from(foreign.taskIds);
foreignTaskIds.splice(destination.index, 0, draggableId);
const newForeign = {
...foreign,
taskIds: foreignTaskIds,
};
const newState = {
...this.state,
columns: {
...this.state.columns,
[newForeign.id]: newForeign,
},
};
this.setState(newState);
};
deleteHandler = (taskId) => {
console.warn("I am going to delete: " + taskId);
var columnId = 'column-2';
const column = this.state.columns[columnId];
const columnTaskIds = Array.from(column.taskIds);
columnTaskIds.splice(columnTaskIds.indexOf(taskId), 1);
const newcolumn = {
...column,
taskIds: columnTaskIds,
};
var newState = null;
newState = {
...this.state,
columns: {
...this.state.columns,
[newcolumn.id]: newcolumn
}
};
this.setState(newState);
console.log(newState);
}
render() {
return (
<DragDropContext
onDragStart={this.onDragStart}
onDragEnd={this.onDragEnd}
>
<Container>
{this.state.columnOrder.map((columnId, index) => {
const column = this.state.columns[columnId];
const tasks = column.taskIds.map(
taskId => this.state.tasks[taskId],
);
const isDropDisabled = index < this.state.homeIndex;
return (
<Column
key={column.id}
column={column}
tasks={tasks}
isDropDisabled={isDropDisabled}
deleteHandler={this.deleteHandler}
/>
);
})}
</Container>
</DragDropContext>
);
}
}
export default App
column.jsx
import React from 'react';
import styled from 'styled-components';
import { Droppable } from 'react-beautiful-dnd';
import Task from './task';
const Container = styled.div`
margin: 8px;
border: 1px solid lightgrey;
border-radius: 2px;
width: 220px;
display: flex;
flex-direction: column;
`;
const Title = styled.h3`
padding: 8px;
`;
const TaskList = styled.div`
padding: 8px;
transition: background-color 0.2s ease;
background-color: ${props => (props.isDraggingOver ? 'skyblue' : 'white')};
flex-grow: 1;
min-height: 100px;
`;
export default class Column extends React.Component {
isSelectedTasksColumn = this.props.column.id === 'column-2';
render() {
return (
<Container>
<Title>{this.props.column.title}</Title>
<Droppable
droppableId={this.props.column.id}
isDropDisabled={this.props.isDropDisabled}
>
{(provided, snapshot) => (
<TaskList
ref={provided.innerRef}
{...provided.droppableProps}
isDraggingOver={snapshot.isDraggingOver}
>
{this.props.tasks.map((task, index) => (
<Task key={task.id} task={task} index={index} isSelectedTasksColumn={this.isSelectedTasksColumn} deleteHandler={this.props.deleteHandler}/>
))}
{provided.placeholder}
</TaskList>
)}
</Droppable>
</Container>
);
}
}
task.jsx
import React from 'react';
import styled from 'styled-components';
import { Draggable } from 'react-beautiful-dnd';
const Container = styled.div`
border: 1px solid lightgrey;
border-radius: 2px;
padding: 8px;
margin-bottom: 8px;
background-color: ${props =>
props.isDragDisabled
? 'lightgrey'
: props.isDragging
? 'lightgreen'
: 'white'};
`;
const DeleteButton = styled.button`
justify-self: center;
align-self: center;
border: 0;
background-color: black;
color: white;
padding: 7px;
cursor: pointer;
display: ${props => props.isSelectedTasksColumn ? "inline" : "none"};
float: right;
`;
const DisplayValue = styled.p`
align-self: center;
font-size: 20px;
padding: 0;
margin: 0;
display: inline;
`;
export default class Task extends React.Component {
render() {
const isDragDisabled = false;
return (
<Draggable
draggableId={this.props.task.id}
index={this.props.index}
isDragDisabled={isDragDisabled}
>
{(provided, snapshot) => (
<Container
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
isDragging={snapshot.isDragging}
isDragDisabled={isDragDisabled}
>
<DisplayValue>{this.props.task.content}</DisplayValue>
<DeleteButton isSelectedTasksColumn = {this.props.isSelectedTasksColumn}
onClick={() => this.props.deleteHandler(this.props.task.id)}>
✖
</DeleteButton>
</Container>
)}
</Draggable>
);
}
}
initial-data.js:
const initialData = {
tasks: {
'task-1': { id: 'task-1', content: 'Task 1' },
'task-2': { id: 'task-2', content: 'Task 2' },
'task-3': { id: 'task-3', content: 'Task 3' },
'task-4': { id: 'task-4', content: 'Task 4' },
},
columns: {
'column-1': {
id: 'column-1',
title: 'All tasks',
taskIds: ['task-1', 'task-2', 'task-3', 'task-4'],
},
'column-2': {
id: 'column-2',
title: 'Selected tasks',
taskIds: [],
}
},
// Facilitate reordering of the columns
columnOrder: ['column-1', 'column-2'],
};
export default initialData;
Removing React.StrictMode could help
https://reactjs.org/docs/strict-mode.html
Context:
I have a small piece of code that I wrote to visualise some binary numbers. It's a self contained HTML file that should work in the browser.
Output:
Question:
How can I show the "Input" block (.input-block div) in each row of this flexbox layout?
Is this achievable using Flexbox?
.container {
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
align-items: flex-start;
height: 80vh;
}
.block {
padding: 15px;
flex: 0 1 150px;
}
.input-block {
border: 1px solid black;
}
.block:hover {
background-color: rgb(231, 231, 231);
}
.row {
padding: 10px;
text-align: right;
color: rgb(61, 61, 61);
letter-spacing: 1.4px;
}
.row:first-child {
font-weight: bold;
color: rgb(83, 83, 83);
background-color: burlywood;
}
.row:nth-child(even) {
background-color: rgb(255, 241, 219);
}
.row-head:nth-child(even) {
background-color: rgb(205, 255, 200);
}
<body>
<div id="root"></div>
</body>
<script type="module">
import { html, render } from 'https://unpkg.com/lit-html?module';
function main() {
const arr = new Array(32).fill(0).map((x, i) => i);
const elements = arr.map(x => getBinaryAssociates(x));
let state = { elements }
display(state);
}
/**
* #param { { elements: ReturnType<typeof getBinaryAssociates>[] } } state
*/
function display(state) {
const root = document.getElementById("root");
/** #type {(_: typeof state) => any} */
const tmpl = (state) => html`<div class="container">
${getHeaderBlockTmpl()}
${state.elements.map(getItemBlockTmpl)}
<div>`;
render(tmpl(state), root);
}
function getHeaderBlockTmpl() {
return html`<div class="block input-block">
<div class="row row-head">Input</div>
${FUNCTIONS.map(x => html`<div class="row row-head">${x.toString()}</div>`)}
</div>`;
}
function getItemBlockTmpl(item) {
return html`<div class="block">
<div class="row">
${numToString(item.input)} ➡ ${item.binary}
</div>
${item.outputs.map(x => html`<div class="row">
${x.value} ➡ ${x.binary}
</div>`)}
</div>`;
}
const FUNCTIONS = [
x => x + 1,
x => x - 1,
x => x & (x - 1),
x => ~x + 1,
x => -(x + 1),
x => ~x,
];
function numToString(n) {
return n.toString();
}
function getBinaryAssociates(number) {
const binary = (x) => (x >>> 0).toString(2).slice(-6).padStart(6, '0');
let res = FUNCTIONS
.map(x => ({
function: x,
value: numToString(x(number)),
binary: binary(x(number)),
}));
let result = res.reduce((acc, n) => {
acc.outputs.push(n);
return acc;
}, {
input: number,
binary: binary(number),
/** #type {typeof res} */
outputs: [],
});
return result;
}
function init() {
document.addEventListener("DOMContentLoaded", () => {
main();
});
}
init();
</script>
Im new to React and started working on a memory game where you flip cards and compare two cards. Im having trouble understanding how to change state of individual component. now when I click a component the state of all components change and all my cards turn red instead of one. later I was thinking to add photos but for now just testing with background color. Also I know I have to add some logic/features but cant get past state problem.
App.js
import React, {Component} from 'react';
import './App.css';
import Grid from './grid/grid';
import Header from './Header/header';
import Footer from './Footer/footer';
class App extends Component {
cards = [{id:1, name: 'dog'},{id:2, name: 'dog'},{id:3, name: 'cat'},{id:4, name: 'cat'},{id:5, name: 'mouse'},{id:6, name: 'mouse'},{id:7, name: 'horse'},{id:8, name: 'horse'},
{id:9, name: 'pig'},{id:10, name: 'pig'},{id:11, name: 'chicken'},{id:12, name: 'chicken'},{id:13, name: 'cow'},{id:14, name: 'cow'},{id:15, name: 'fox'},{id:16, name: 'fox'}]
.sort( () => Math.random() - 0.5);
clicks = [];
state = {
current: 0,
}
clickHandler = (click) => {
this.clicks.push(click.name);
this.setState({
current: 1
})
console.log(this.clicks);
if (this.clicks.length > 1) {
this.compare(click.name);
}
}
compare = (name) => {
if (name === this.clicks[0]) {
console.log('pair')
} else {
console.log('nope');
}
}
render() {
return (
<div className="App">
<Header />
<div className='Grid-container'>
<div className='wrapper'>
{this.cards.map(child =>
<Grid click={() => this.clickHandler(child)}
active={this.state.current === 0}
id={child.id}
/>)}
</div>
</div>
<Footer />
</div>
);
}
}
export default App;
grid.js
import React from 'react';
import './grid.css';
const Grid = (props) => {
return (
<div className={'Child' + (props.active ? '': ' active')}
onClick={props.click}
>
{props.id}
</div>
);
}
export default Grid;
App.css
.Grid-container {
display: flex;
background-color: black;
justify-content: center;
align-items: center;
}
.wrapper {
display: grid;
width: 700px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
grid-gap: 10px;
background-color: black;
justify-content: center;
align-items: center;
}
**grid.css**
.Child {
width: auto;
height: 120px;
background-color: azure;
border-radius: 10px;
}
.Child.active {
width: auto;
height: 120px;
background-color: red;
border-radius: 10px;
}
You have to use index for this. You have index as second argument in map,
{this.cards.map( ( child, index ) =>
<Grid
click={() => this.clickHandler(child,index)}
active={this.state.current === index}
id={child.id}
key ={child.id} //Provide unique key here
/>
)}
Your click hander should be,
clickHandler = (click,index) => {
this.clicks.push(click.name);
this.setState({
current: index //Set index in a state
})
console.log(this.clicks);
if (this.clicks.length > 1) {
this.compare(click.name);
}
}
You need to initialize current state as empty, otherwise you will get first grid by default active
state = {
current: '',
}
Your Grid component will be this,
const Grid = (props) => {
return (
<div
className={`Child ${props.active ? 'active': ''}`} //Your condition should be this
onClick={props.click}
>
{props.id}
</div>
);
}
Demo
Updating state section
state = {
current: -1, // nothing is selected it contains id of selected card
clicks: [],
}
clickHandler = (selectedId) => {
const { clicks, current } = this.state;
if (current === -1) { // no card selected to check
this.setState({
current: selectedId,
clicks: clicks.includes(selectedId) ? clicks : [...clicks, selectedId],
});
return; // no more in this funtion
}
if (selectedId === current) { // already selected card(totally same card)
this.setState({
current: -1, // unselect last selected card :(
clicks: clicks.slice(0, clicks.length - 1), // remove last selected click
});
} else { // different card. here check if they have same name
if (this.cards[selectedId].name === this.cards[current].name) {
// couple cards
console.log('Bingo They are Same!!!');
this.setState({
current: -1,
clicks: [...cliks, selectedId], // add just selected card in cliks
});
} else {
// Oh, you failed!
this.setState({
current: -1, // unselect last selected card :(
clicks: clicks.slice(0, clicks.length - 1),
});
}
}
}
Render
...
<Grid
click={() => this.clickHandler(child.id)}
active={this.state.clicks.includes(child.id)} // check if it is included in clicks
id={child.id}
/>
...
Then you can see cool cards game. :)
I am trying to replace a single item in my array with another item, I have a placeholder with a number position on it, and when I click on add the first item should go into the 1st position , 2nd item into the 2nd position and so on.
At the moment it will add the item in all positions when clicked on, which is not the behaviour I would like.
If I remove the number position placeholder I can get them to go into the array in the correct position but I'm unable to get it to work with the placeholder.
How can I get the product item when clicked on replace the number position in the array?
https://codesandbox.io/s/6z48qx8vmz
Hello.js
import React from 'react';
import update from 'immutability-helper'
import styled from 'styled-components'
import Product from './Product'
const NumberWrap = styled.div`
display: flex;
flex-wrap:wrap
border: 1px solid #ccc;
flex-direction: row;
`
const Numbers = styled.div`
display:flex;
background: #fafafa;
color: #808080;
font-size: 32px;
flex: 0 0 20%;
min-height: 200px;
justify-content: center;
align-items:center;
border-right: 1px solid #ccc;
`
const CardWrap = styled.div`
display:flex;
flex-wrap: wrap;
flex-direction: row;
margin-top: 20px;
`
export default class Hello extends React.Component {
constructor() {
super()
this.state = {
placeholder: [1,2,3,4,5],
data: [
{ id: 1, header: 'Item 1'},
{ id: 2, header: 'Item 2'},
{ id: 3, header: 'Item 3'},
{ id: 4, header: 'Item 4'}
],
addedItems: [],
}
this.handleAdd.bind(this)
this.handleRemove.bind(this)
}
handleAdd(id) {
this.setState({
addedItems: update(this.state.addedItems, {
$push: [
id,
],
})
})
}
handleRemove(index) {
this.setState({
addedItems: update(this.state.addedItems, {
$splice: [
[index, 1]
],
})
})
}
render() {
return(
<div>
<NumberWrap>
{
this.state.placeholder.map(item =>
<Numbers>{item}
{
this.state.data.filter(item =>
this.state.addedItems.indexOf(item.id) !== -1).slice(0, 5).map(item =>
<Product {...item} remove={this.handleRemove} />
)
}
</Numbers>
)}
</NumberWrap>
<CardWrap>
{
this.state.data.map(item =>
<Product {...item} add={()=>this.handleAdd(item.id)} />
)}
</CardWrap>
</div>
)
}
}
Product.js
import React from "react";
import styled from "styled-components";
const Card = styled.div`
flex: 0 0 20%;
border: 1px solid #ccc;
`;
const Header = styled.div`
padding: 20px;
`;
const AddBtn = styled.button`
width:100%;
height: 45px;
`;
const Product = props => {
const { add, id, header, remove } = props;
return (
<Card>
<Header key={id}>
{header}
</Header>
<AddBtn onClick={add}>Add</AddBtn>
<AddBtn onClick={remove}>Remove</AddBtn>
</Card>
);
};
export default Product;
I had bit hard time understanding the question but is this what you want https://codesandbox.io/s/vj7vxw198y ? It still would need some work to allow adding same item multiple times if you want that.
I have following UI element on the top of my page:
|[Static1] [Dynamic1] [Dynamic2] [Dynamic3] [Static2]|
So Static1 is some logo component that sticks to the left, Static2 is some user menu component that sticks to the right.
Now inside of it I have a collection component that displays several dynamic elements loaded from the DB.
All is good, if there are not too much of those components, but if there are more, I don't wan't any linebreaks, only some fort of "More" menu, something like:
|[Static1] [Dynamic1] [Dynamic2] [Dynamic3] [Dynamic4][...][Static2]|
and when I click the [...] button I wan't a vertical list of the dynamic components.
The list of dynamic items is stored in an ElementList component, with following code:
React.createClass({
render() {
return (
<div ref="listparent">
{ this.props.elements.map((element) => {
return <Element
ref={"element-"+element.name}
key={element.name}
})}
</div>
)
}
});
this.props.elements is a collection passed as a prop. I tried something allong those lines, but it either didn't work or worked but not on each page refresh:
export default React.createClass({
getInitialState(){
return {
visibleElements: this.props.elements,
hiddenElements: []
}
},
componentDidMount() {
this.rearrange();
},
componentDidUpdate(){
this.rearrange();
},
rearrange(){
var element = ReactDOM.findDOMNode(this.refs.listparent);
let visibleElements = [];
let hiddenElements = [];
for(var i=0; i< this.props.elements.length; i++)
{
var currentElement = this.props.elements[i];
var domElement = ReactDOM.findDOMNode(this.refs["element-"+element.name]);
if(domElement) {
if (domElement.offsetTop + domElement.offsetHeight >
element.offsetTop + element.offsetHeight ||
domElement.offsetLeft + domElement.offsetWidth >
element.offsetLeft + element.offsetWidth - 200) {
hiddenElements.push(currentElement);
}
else {
visibleElements.push(currentElement);
}
}
}
if(this.state.visibleElements.length != visibleElements.length) {
this.setState({
visibleElements: visibleElements,
hiddenElements: hiddenElements
})
}
},
render() {
return (
<div ref="listparent">
{ this.state.visibleElements.map((element) => {
return <Element
ref={"element-"+element.name}
key={element.name} />
})}
{ this.state.hiddenElements.length >0 &&
<DropdownMenu
Header="..."
>
{ this.state.hiddenElements.map((element) => {
return <Element
ref={"element-"+element.name}
key={element.name} />
})}
</DropdownMenu>
}
</div>
)
}
});
Here is a rough jsFiddle with what I want to do: https://jsfiddle.net/3uf9r8ne/
Got it working, I don't know if that's the best solution, or how robust it is, but works for me at least for now.
JsFiddle: https://jsfiddle.net/1w6m1n6h/
var Dropdown = React.createClass({
getInitialState(){
return {
isOpen: false
}
},
componentWillMount() {
document.addEventListener('click', this.handleClick, false);
},
componentWillUnmount() {
document.removeEventListener('click', this.handleClick, false);
},
handleClick: function (e) {
var component = ReactDOM.findDOMNode(this.refs.component);
if (e.target == component || $(component).has(e.target).length) {
// Inside the component
}
else{
// Outide
this.setState({ isOpen: false});
}
},
render()
{
return (
<div ref="component" className="dropdown">
<div className="dropdown-button" onClick={() => this.setState({ isOpen : !this.state.isOpen})}>{this.props.Header}</div>
{
this.state.isOpen && (
<div className="dropdown-menu" onClick={() => this.setState({ isOpen : false})}>
{React.Children.map(this.props.children, (item) => item)}
</div>
)
}
</div>
);
}
});
var Card = React.createClass({
render() {
let className = "card";
if(this.props.isHidden)
className += " is-hidden";
return (
<div className={className}>{this.props.name}</div>
)
}
});
var Cards = React.createClass({
getInitialState() {
return {
vCards: [],
hCards: [],
lastSetCards: [],
preMounted: false,
laidOut: false
};
},
rearrange() {
_.throttle(this.setState({laidOut: false, preMounted: false}), 100);
},
componentDidMount() {
window.addEventListener('resize', this.rearrange);
},
componentWillUnmount() {
window.removeEventListener('resize', this.rearrange);
},
componentDidUpdate() {
if(this.props.cards.length != this.state.lastSetCards || !this.state.preMounted) {
this.setState({
lastSetCards: this.props.cards.length,
vCards: this.props.cards,
preMounted: true,
laidOut: false
});
}
if(this.state.preMounted && !this.state.laidOut) {
var element = ReactDOM.findDOMNode(this.refs.listparent);
let visibleCards = [];
let hiddenCards = [];
for(var i=0; i< this.props.cards.length; i++)
{
var card = this.props.cards[i];
var cardElement = ReactDOM.findDOMNode(this.refs["card-"+card]);
if(cardElement) {
if (cardElement.offsetTop + cardElement.offsetHeight >
element.offsetTop + element.offsetHeight ||
cardElement.offsetLeft + cardElement.offsetWidth >
element.offsetLeft + element.offsetWidth - 160) {
hiddenCards.push(card);
}
else {
visibleCards.push(card);
}
}
}
this.setState({
vCards: visibleCards,
hCards: hiddenCards,
laidOut: true
});
}
},
render() {
return (<div className="cards-top" ref="listparent">
<div className="cards" >
{this.state.vCards.map((c)=> <Card ref={"card-"+c} key={c} name={c} />)}
</div>
<Dropdown Header="MORE">
{this.state.hCards.map((c)=> <Card isHidden={true} key={c} name={c} />)}
</Dropdown>
</div>
)
}
});
var Hello = React.createClass({
getInitialState() {
return {
cards: ["one", "two" ]
};
},
componentDidMount() {
this.setState({
cards: ["one", "two", "three", "four", "five", "six", "seven", "eight",
"nine", "ten", "eleven", "twelve", "thirteen", "fourteen"]
});
},
render: function() {
let addNew = () => {
this.state.cards.push("additional_"+this.state.cards.length);
this.setState({
cards: this.state.cards
})
};
return (
<div>
<div className="header">
<div className="logo">Logo</div>
<div className="user">User</div>
<Cards cards={this.state.cards} />
<div className="clear"></div>
</div>
<br/><br/>
<button onClick={addNew}>Add</button>
</div>);
}
});
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
.logo
{
float: left;
margin: 5px;
padding: 5px;
border: solid 1px blue;
}
.user
{
float: right;
margin: 5px;
padding: 5px;
border: solid 1px blue;
}
.header{
position: relative;
max-height: 10px;
height: 10px;
width: 100%;
}
.cards
{
position: relative;
display: inline-block;
vertical-align: top;
white-space: nowrap;
}
.clear
{
clear: both;
}
.card
{
display: inline-block;
margin: 5px;
padding: 5px;
border: solid 1px blue;
}
.cards-top
{
display: block;
white-space: nowrap;
vertical-align: top;
width: 100%;
border: green 1px solid;
}
.dropdown
{
display: inline-block;
}
.is-hidden
{
display: block;
}
.dropdown-button
{
margin: 5px;
padding: 5px;
border: solid 1px blue;
}
<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>