draftjs entity Content State to HTML - javascript

I am using draftjs in which users can type in anything and can also click a button on which I am inserting a IMMUTABLE entity.
const text = "foo";
const editorState = this.state.value;
const selectionState = editorState.getSelection();
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity("TOKEN", "IMMUTABLE", { time: new Date().getTime() });
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const modifiedContent = Modifier.insertText(contentState, selectionState, text, OrderedSet([ "INSERT" ]), entityKey);
const nextState = EditorState.push( editorState, modifiedContent, editorState.getLastChangeType() );
this.setState({value: nextState}, this.focus );
https://codepen.io/dakridge/pen/XgLWJQ
All this is working fine but when the editor state is saved and now I am trying to render the HTML from the saved ContentState in my webpage then I am not able to identify the immutable entity and apply styles to it or render it differently.
For ex, in the above example how can foo be rendered with a different color and how the saved timestamp say can be logged in the console when I hover over foo?
I am using draftjs-to-html to render html from draftjs output.

If you want to track the timestamp (and other data of immutability) then your best option is to store the "raw" form instead of html (or keep alongside the html). You can use the convertToRaw to get the raw content, here's an example of foo inserted with timestamp as epoch.
{
"entityMap": {
"0": {
"type": "TOKEN",
"mutability": "SEGMENTED",
"data": {
"time": 1657116932641
}
}
},
"blocks": [
{
"key": "ekpc6",
"text": "aaa",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [],
"data": {}
},
{
"key": "dmkkr",
"text": "foo",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [
{
"offset": 0,
"length": 3,
"style": "INSERT"
}
],
"entityRanges": [
{
"offset": 0,
"length": 3,
"key": 0
}
],
"data": {}
}
]
};
I've taken your codepen code & added an explicit button to get raw content. Once you get the raw JSON, you can extract any meta data out.
const { Editor, EditorState, Modifier, convertToRaw, convertFromRaw } = Draft;
const { OrderedSet } = Immutable;
const sample_saved_state = {
"entityMap": {
"0": {
"type": "TOKEN",
"mutability": "SEGMENTED",
"data": {
"time": 1657116932641
}
}
},
"blocks": [
{
"key": "ekpc6",
"text": "aaa",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [],
"data": {}
},
{
"key": "dmkkr",
"text": "foo",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [
{
"offset": 0,
"length": 3,
"style": "INSERT"
}
],
"entityRanges": [
{
"offset": 0,
"length": 3,
"key": 0
}
],
"data": {}
}
]
};
class EditorComponent extends React.Component {
constructor(props) {
super(props);
// In case you store the sample_saved_state as string in server side, you might have to do JSON.parse
console.log("Converting from saved state");
const saved_state = sample_saved_state != null ? EditorState.createWithContent(convertFromRaw(sample_saved_state))
: EditorState.createEmpty();
console.log("Assining to state ");
this.state = {
value : saved_state
};
this.saveContent = this.saveContent.bind(this);
//this.state = { value: EditorState.createEmpty() };
this.onChange = (value) => this.setState({value});
this.focus = () => this.refs.editor.focus();
this.insert = this.insert.bind( this );
}
insert () {
const text = "foo";
const editorState = this.state.value;
const selectionState = editorState.getSelection();
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity("TOKEN", "SEGMENTED", { time: new Date().getTime() });
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const modifiedContent = Modifier.insertText(contentState, selectionState, text, OrderedSet([ "INSERT" ]), entityKey);
const nextState = EditorState.push( editorState, modifiedContent, editorState.getLastChangeType() );
this.setState({value: nextState}, this.focus );
}
saveContent () {
console.log("inside saveContent");
const content = convertToRaw(this.state.value.getCurrentContent());
// you can save the content as json in server side
console.log("Raw Content: ",content)
}
render () {
return (
<div>
<div onClick={ this.focus } className="editor">
<Editor
ref="editor"
onChange={ this.onChange }
editorState={ this.state.value }
customStyleMap={{ "INSERT": { backgroundColor: "yellow", padding: "0 2px" } }}
/>
</div>
<button onClick={ this.insert }>Insert</button>
<button onClick={this.saveContent}>Save</button>
</div>
);
}
}
ReactDOM.render(
<EditorComponent />,
document.getElementById('app')
);

Solution codesandbox: https://codesandbox.io/s/staging-fire-wo3oc5?file=/src/Editor.js
Instead of draftjs-to-html, I recommend draft-convert that comes with the functionality you want. You can customize the output HTML based on style and entity.
const html = convertToHTML({
styleToHTML: (style) => {
if (style === 'INSERT') {
return <span style={{ backgroundColor: 'yellow', padding: '0 2px' }} />;
}
},
entityToHTML: (entity) => {
if (entity.type === 'TOKEN') {
return <span data-time={entity.data.time} className="entity-token" />;
}
},
})(this.state.value.getCurrentContent());
For instance, I am wrapping nodes with the INSERT style inside a styling span, and nodes that correspond with the entity type TOKEN inside a functional span. This will result in an HTML node that looks like this:
Of course, you can combine these logic to wrap your token nodes inside 1 span only, if you want.
The reason we need functional spans is because handling interactions (like displaying time in console on hover) needs custom JavaScript, and we need to do this the old fashioned way. Where you render the HTML, just add the following effect hook:
useEffect(() => {
const tokens = [
...containerRef.current.getElementsByClassName("entity-token")
];
const handler = (event) => {
const timestamp = event.target.dataset.time;
console.log(timestamp);
};
tokens.forEach((token) => {
token.addEventListener("mouseenter", handler);
});
return () => {
tokens.forEach((token) => {
token.removeEventListener("mouseenter", handler);
});
};
}, [content]);
This should handle the interactions for you. Sorry for mixing class components and functional components with hooks, but you can achieve the same thing with class components (using componentDidMount and componentDidUpdate) as well. Using hook is just more elegant.

Related

React Flow - How to use data in Edge With Button?

I create a web interface, allowing to create graphs from boxes that I created.
I arrive at a stage where I want to create an Edge With Button, the button is for example a checkbox. And I would like to retrieve the information from this checkbox in my data.
To redo the example you can use the "Edge With Button" documentation from the React Flow library. (https://reactflow.dev/examples/edge-with-button)
My code is exactly the same, except that I added a checkbox in the button.
I also create a function in my hand that allows me to retrieve my items.
const sendGraph = () => {
console.log(elements)
}
[
{
"id": "dndnode_3",
"type": "input",
"position": {
"x": 180,
"y": 150
},
"data": {
"label": "node3"
}
},
{
"id": "dndnode_4",
"type": "default",
"position": {
"x": 240,
"y": 270
},
"data": {
"label": "node4"
}
},
{
"source": "dndnode_3",
"sourceHandle": null,
"target": "dndnode_4",
"targetHandle": null,
"id": "reactflow__edge-dndnode_3null-dndnode_4null",
"type": "buttonedge"
}
]
I found a way to add a new field, like "data", but this is done when creating the Edge and so I don't have the checkbox value. I get an Edge like this:
{
"data" : "Hello"
"source": "dndnode_3",
"sourceHandle": null,
"target": "dndnode_4",
"targetHandle": null,
"id": "reactflow__edge-dndnode_3null-dndnode_4null",
"type": "buttonedge"
}
Environment :
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-flow-renderer": "^9.6.7",
"react-scripts": "4.0.3"
How can I retrieve the value of my checkbox and assign it for example to "data"?
Thanks in advance.
I have found a solution, but I do not think it is a good practice at all.
Index.js
I added the "data" parameter in the "addEdge" method, with a "checkbox" variable initialized to "False".
import React, { useState } from 'react';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
Controls,
Background,
} from 'react-flow-renderer';
import ButtonEdge from './ButtonEdge';
const onLoad = (reactFlowInstance) => reactFlowInstance.fitView();
const initialElements = [
{
id: 'ewb-1',
type: 'input',
data: { label: 'Input 1' },
position: { x: 250, y: 0 },
},
{ id: 'ewb-2', data: { label: 'Node 2' }, position: { x: 250, y: 300 } },
{
id: 'edge-1-2',
source: 'ewb-1',
target: 'ewb-2',
type: 'buttonedge',
},
];
const edgeTypes = {
buttonedge: ButtonEdge,
};
const EdgeWithButtonFlow = () => {
const [elements, setElements] = useState(initialElements);
const onElementsRemove = (elementsToRemove) =>
setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params) =>
setElements((els) => addEdge({ ...params, type: 'buttonedge', data:{ "checkbox": False} }, els));
return (
<ReactFlow
elements={elements}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
snapToGrid={true}
edgeTypes={edgeTypes}
onLoad={onLoad}
key="edge-with-button"
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
);
};
export default EdgeWithButtonFlow;
ButtonEdge.js
I created a useState which allows me to manage the state of my checkbox, and during the update I modify the value of data.checkbox. If I do everything via data.checkbox, obviously the parent does not update the state of the checkbox, since he is not aware of it.
import React from 'react';
import {
getBezierPath,
getEdgeCenter,
getMarkerEnd,
} from 'react-flow-renderer';
import './index.css';
const foreignObjectSize = 40;
const onEdgeClick = (evt, id) => {
evt.stopPropagation();
alert(`remove ${id}`);
};
export default function CustomEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
data,
arrowHeadType,
markerEndId,
}) {
const edgePath = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const [edgeCenterX, edgeCenterY] = getEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
});
const [checkState, setCheckState] = useState("False")
const updateCheckBox= (evt, id) => {
evt.stopPropagation();
setCheckState(!checkState)
data.checkbox= !data.checkbox
};
return (
<>
<path
id={id}
style={style}
className="react-flow__edge-path"
d={edgePath}
markerEnd={markerEnd}
/>
<foreignObject
width={foreignObjectSize}
height={foreignObjectSize}
x={edgeCenterX - foreignObjectSize / 2}
y={edgeCenterY - foreignObjectSize / 2}
className="edgebutton-foreignobject"
requiredExtensions="http://www.w3.org/1999/xhtml"
>
<body>
<button
className="edgebutton"
onClick={(event) => onEdgeClick(event, id)}
>
<CheckBox onClick={(event) => updateCheckBox(event, id)} value={checkState}/>
</button>
</body>
</foreignObject>
</>
);
}
If there is another way, maybe more "better practice friendly", I am interested.

How can I convert html to draftjs?

I have tried basic packages but I don't seem to get whats going on,
Here is something that I tried;
const {
convertFromHTML,
ContentState
} = require('draft-js');
const htmlToDraft = require('html-to-draftjs');
const converter = () => {
const sampleMarkup =
'<b>Bold text</b>, <i>Italic text</i><br/ ><br />' +
'Example link';
const blocksFromHTML = convertFromHTML(sampleMarkup);
const state = ContentState.createFromBlockArray(blocksFromHTML);
console.log('state: ', state);
}
converter();
It was really clear on which library to use.
I am getting weird looking outputs, what I expect looks something like this;
{
"blocks": [
{
"depth": 0,
"inlineStyleRanges": [
{
"length": 9,
"style": "BOLD",
"offset": 0
},
{
"length": 12,
"style": "ITALIC",
"offset": 11
}
],
"entityRanges": [
{
"length": 12,
"key": 0,
"offset": 25
}
],
"data": {},
"text": "Bold text, Italics text\n\nexample link ",
"key": "9jc4q",
"type": "unstyled"
}
],
"entityMap": {
"0": {
"type": "LINK",
"mutability": "MUTABLE",
"data": {
"url": "http://www.google.com",
"targetOption": "_blank"
}
}
}
}
Any insights ? (code in server side )
const sampleMarkup =
'<b>Bold text</b>, <i>Italic text</i><br/ ><br />' +
'Example link';
const blocksFromHTML = convertFromHTML(sampleMarkup);
const state = ContentState.createFromBlockArray(
blocksFromHTML.contentBlocks,
blocksFromHTML.entityMap,
);
this.state = {
editorState: EditorState.createWithContent(state),
};
Please use this function. 100% working
import htmlToDraft from 'html-to-draftjs';
import { ContentState, EditorState } from 'draft-js';
const htmlToDraftBlocks = (html) => {
const blocksFromHtml = htmlToDraft(html);
const { contentBlocks, entityMap } = blocksFromHtml;
const contentState = ContentState.createFromBlockArray(contentBlocks, entityMap);
const editorState = EditorState.createWithContent(contentState);
return editorState;
}
useEffect(()=>{
setEditorState(htmlToDraftBlocks("<p>Hello</p>"));
},[])

Material Table Get and Set Filter Values

How can I get and set the filter values programmatically using material-table?
I want users to be able to save filter configurations as reports and recall them as needed.
Get works with a hook on change:
onFilterChange={(filters) => {
console.log('onFilterChange', filters);
}}
result is an array of filter definitions per column, looks like:
[
// [...]
{
"column": {
"title": "Date",
"field": "file_date",
"type": "date",
"tableData": {
"columnOrder": 3,
"filterValue": "2020-11-10T15:20:00.000Z",
"groupSort": "asc",
"width": "...", // lots of css calc stuff... :(
"additionalWidth": 0,
"id": 4
}
},
"operator": "=",
"value": "checked"
}
]
setting the filter on mount could/should work with defaultFilter at each column definition.
There are two parts to this, the get and the set.
Get - handled through the use of the tableRef prop on the MaterialTable component
Set - handled through the defaultFilter value on a column object.
import MaterialTable from "material-table";
import React, { useRef } from "react";
import { tableIcons } from "./tableIcons";
const firstNameFilter = 'Neil'
function App() {
const tableRef = useRef<any>();
return (
<div>
<button onClick={saveFilters(tableRef)}>Filters</button> // GET OCCURS HERE
<MaterialTable
tableRef={tableRef}
icons={tableIcons}
columns={[
{ title: "First", field: "name", defaultFilter: firstNameFilter }, // SET OCCURS HERE
{ title: "Last", field: "surname" }
]}
data={[
{ name: "Neil", surname: "Armstrong" },
{ name: "Lance", surname: "Armstrong" },
{ name: "Bob", surname: "Hope" }
]}
options={{ filtering: true }}
title="Reports"
/>
</div>
);
}
function saveFilters(tableRef: React.MutableRefObject<any>) {
return function handler() {
const columns = tableRef?.current?.state.columns.map((column: any) => ({
field: column.field,
filterValue: column.tableData.filterValue
}));
console.log(JSON.stringify(columns, null, 2));
};
}
export { App };

Material UI - TreeView datastructure

I want to structure the data that I get from a server, so I can use the TreeView component from Material UI: https://material-ui.com/api/tree-view/
I'm fetching large amounts of data so I want to fetch child nodes from the server when the user clicks on the expand button. So
when the first node is expanded a HTTP request is sent to a server which returns all of the children of that node. When another node is expanded the children of that node is fetched etc.
On startup of the page I want to fetch the root node and its children. The JSON returned will look something like this:
{
"division": {
"id": "1234",
"name": "Teest",
"address": "Oslo"
},
"children": [
{
"id": "3321",
"parentId": "1234",
"name": "Marketing",
"address": "homestreet"
},
{
"id": "3323",
"parentId": "1234",
"name": "Development",
"address": "homestreet"
}
]
}
When expanding the Marketing node I want to make a HTTP call to fetch the children of this node. So I would get JSON like this:
{
"children": [
{
"id": "2212",
"parentId": "3321",
"name": "R&D",
"address": "homestreet"
},
{
"id": "4212",
"parentId": "3321",
"name": "Testing",
"address": "homestreet"
}
]
}
But I am confused on how to create such a data structure which can later be used my the TreeView component. How can I create such a structure?
For anyone still looking for a solution to this problem I've recently tackled it using a combination of the selected and expanded props in the TreeView API. See this Code Sandbox demo for an example of how to asynchronously load new children and expand their parent once they are loaded.
import React from "react";
import TreeView from "#material-ui/lab/TreeView";
import ExpandMoreIcon from "#material-ui/icons/ExpandMore";
import ChevronRightIcon from "#material-ui/icons/ChevronRight";
import TreeItem from "#material-ui/lab/TreeItem";
import TreeNode from "./TreeNode";
const mockApiCall = async () => {
return new Promise((resolve) => {
setTimeout(() => {
const nextId = Math.ceil(Math.random() * 100);
resolve([
{
id: `${nextId}`,
name: `child-${nextId}`,
children: []
},
{
id: `${nextId + 1}`,
name: `child-${nextId + 1}`,
children: []
}
]);
}, Math.ceil(Math.random() * 1000));
});
};
export default class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
expanded: [],
selected: "1",
tree: new TreeNode({
id: "1",
name: "src",
children: []
})
};
}
handleChange = async (event, nodeId) => {
const node = this.state.tree.search(nodeId);
if (node && !node.children.length) {
mockApiCall()
.then((result) => {
this.setState({ tree: this.state.tree.addChildren(result, nodeId) });
})
.catch((err) => console.error(err))
.finally(() => {
this.setState({
selected: nodeId,
expanded: [...this.state.expanded, nodeId]
});
});
}
};
createItemsFromTree = (tree) => {
if (tree.children.length) {
return (
<TreeItem key={tree.id} nodeId={tree.id} label={tree.name}>
{tree.children.length > 0 &&
tree.children.map((child) => this.createItemsFromTree(child))}
</TreeItem>
);
}
return <TreeItem key={tree.id} nodeId={tree.id} label={tree.name} />;
};
render() {
return (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
selected={this.state.selected}
onNodeSelect={this.handleChange}
expanded={this.state.expanded}
>
{this.createItemsFromTree(this.state.tree)}
</TreeView>
);
}
}

Passing list of values as filtertable content

I'm attempting to filter using a list of values with React.
All my "tags" have a "taglevel" to indicate their relevance.
I want it to "cancel out" tags which are the same (ie don't repeat the tag if its' the same).
I want the first row to show all tag.name with "taglevel" of 1.
I want the second row to show all tag.name with "taglevel" of 2 or more.
I am unable to show and filter on the value "tags". Possibly it is around line 145 of my codepen where I have made the error.
Here is what I am trying to achieve:
I've put this together in a codepen.
http://codepen.io/yarnball/pen/GqbyWr?editors=1010
Without success, I have now tried the following:
I tried filtering using this using:
var LevelFilter = React.createClass({
render: function(){
if (!this.props.tags) return null;
return this.props.tags.filter(tag => tag.taglevel === this.props.targetLevel).map(tag => <a onClick={this.props.onClick}>{tag.name}</a>);
}
});
Then trying to get it in my return here:
render: function(){
...
var getUniqueCategories=[];
PHOTODATA.forEach(function(el){
if(getUniqueCategories.indexOf(el.tag) === -1 ) getUniqueCategories.push(el.tag);
})
return (
<div className="overlay-photogallery">
<div className="filter-panel"><b>Tags with taglevel 1 only to be displayed</b>
{
getUniqueCategories.map(function(el,i){
var boundClick = titleToSelect.bind(null,el);
return <LevelFilter onClick={boundClick} targetLevel={1} tags={el.tag} />
})
}
<a className="resetBtn" onClick={this.resetFilter}> Reset Filter </a>
</div>
My data looks like this:
"title": "Into the Wild",
"tag": [
{
"name": "Movie",
"taglevel": 1,
"id": 1
},
{
"name": "Adventure",
"taglevel": 2,
"id": 30
},
{
"name": "Book",
"taglevel": 1,
"id": 2
}
],
"info": []
}
TL;DR
You have some serious issues with your array manipulation and your React components.
Remember that React advocates a specific top down structure and you should read up on it some more. Each React Component should use props as much as possible and ideally only 1 top-level component should hold state.
QUICK ways forward:
Pass all the data down and let each level filter make the list unique.
Seriously, split up your components and let them depend on props as much as possible.
Give variables meaningful names. el is not meaningful and in your case refers to PHOTO items in the PHOTODATA array, tags in a PHOTO and then you use element to mean something else again. Don't go to over the top, but at least be able to identify what the variable is supposed to do.
I've given in and made a codepen with a much updated structure. The behaviour may not be exactly what you're looking for, but look at the code and how it is organised and how information is shared and passed between components.
http://codepen.io/anon/pen/AXGGLy?editors=1010
UPDATE
To allow multiple filters two methods should be updated:
selectTag: function (tag) {
this.setState({
displayedCategories: this.state.displayedCategories.concat([tag])
});
}
tagFilter: function (photo) {
return this.props.displayedCategories.length !== 0 &&
this.props.displayedCategories.every(function(thisTag) {
return photo.tag.some(function (photoTag) {
return photoTag.id === thisTag.id &&
photoTag.taglevel === thisTag.taglevel;
});
});
},
selectTag now appends to the displayedCategories array rather than replacing it.
tagFilter now checks that at least one filter has been applied (remove this.props.displayedCategories.length !== 0 to disable this) so that it doesn't display all by default and then checks that every selected filter is present in each photo, thus making the components additive.
There are further improvements that could be made, such as to disable a level when a filter is applied at that level (one choice per level) or to show a list of applied filters, either through colour on the buttons or a tag list above the results.
(codepen updated with these latest changes)
Ok, there are a few problems with your codepen.
First, on line 137 you extract the tag array from the object:
if(getUniqueCategories.indexOf(el.tag) === -1 ) getUniqueCategories.push(el.tag);
Then, on 146 you extract it again:
return <LevelFilter onClick={boundClick} targetLevel={1} tags={el.tag} />
and again for level 2:
return <LevelFilter onClick={boundClick} targetLevel={2} tags={el.tag} />
For both of these it should be:
return <LevelFilter onClick={boundClick} targetLevel={n} tags={el} />
Which then allows another problem to manifest itself, which is that LevelFilter doesn't return a valid React component (an array is not valid).
return this.props.tags.filter(tag => tag.taglevel === this.props.targetLevel).map(tag => <a onClick={this.props.onClick}>{tag.name}</a>);
should be
return (
<div>
{
this.props.tags
.filter(tag => tag.taglevel === this.props.targetLevel)
.map(tag => <a onClick={this.props.onClick}>{tag.name}</a>)
}
</div>
);
After these changes you should have a much closer attempt to where you want to be.
There are further issues you will need to look into, things like your boundClick function won't work correctly because you only have a list of tags, not PHOTODATA.
However, just a final thought. You might want to break your React components up a little more.
For reference, here is the full code listing from the codepen:
var PHOTODATA = [{
"title": "Into the Wild",
"tag": [
{
"name": "Movie",
"taglevel": 1,
"id": 1
},
{
"name": "Adventure",
"taglevel": 2,
"id": 30
},
{
"name": "Book",
"taglevel": 1,
"id": 2
}
],
"info": []
},{
"title": "Karate Kid",
"tag": [
{
"name": "Movie",
"taglevel": 1,
"id": 1
},
{
"name": "Adventure",
"taglevel": 2,
"id": 30
},
{
"name": "Kids",
"taglevel": 3,
"id": 4
}
],
"info": []
},
{
"title": "The Alchemist",
"tag": [
{
"name": "Book",
"taglevel": 1,
"id": 2
},
{
"name": "Adventure",
"taglevel": 2,
"id": 30
},
{
"name": "Classic",
"taglevel": 2,
"id": 4
},
{
"name": "Words",
"taglevel": 4,
"id": 4
}
],
"info": []
}];
var PhotoGallery = React.createClass({
getInitialState: function() {
return {
displayedCategories: []
};
},
selectTag: function (tag) {
this.setState({
displayedCategories: this.state.displayedCategories.concat([tag])
});
},
resetFilter: function(){
this.setState({
displayedCategories: []
});
},
render: function(){
var uniqueCategories = PHOTODATA.map(function (photo) {
return photo.tag; // tag is a list of tags...
}).reduce(function (uniqueList, someTags) {
return uniqueList.concat(
someTags.filter(function (thisTag) {
return !uniqueList.some(function(uniqueTag) {
return uniqueTag.id === thisTag.id && uniqueTag.taglevel === thisTag.taglevel
});
})
);
}, []);
return (
<div className="overlay-photogallery">
<div className="filter-panel"><b>Tags with taglevel 1 only to be displayed</b>
<PhotoGalleryLevel level={1} tags={uniqueCategories} displayedCategories={this.state.displayedCategories} selectTag={this.selectTag} />
<a className="resetBtn" onClick={this.resetFilter}> Reset Filter </a>
</div>
<div className="filter-panel"><b>Tags with taglevel 2 only to be displayed</b>
<PhotoGalleryLevel level={2} tags={uniqueCategories} displayedCategories={this.state.displayedCategories} selectTag={this.selectTag} />
</div>
<div className="PhotoGallery">
<PhotoDisplay displayedCategories={this.state.displayedCategories} photoData={PHOTODATA} />
</div>
</div>
);
}
});
var PhotoGalleryLevel = React.createClass({
render: function () {
var filteredTags = this.props.tags.filter(function (tag) {
return tag.taglevel === this.props.level;
}.bind(this));
var disabled = this.props.displayedCategories.some(function (tag) {
return tag.taglevel === this.props.level;
}.bind(this));
return (
<div>
{filteredTags.map(function (tag){
return <PhotoGalleryButton tag={tag} selectTag={this.props.selectTag} disabled={disabled} />;
}.bind(this))}
</div>
);
}
});
var PhotoGalleryButton = React.createClass({
onClick: function (e) {
this.props.selectTag(this.props.tag);
},
render: function () {
return (
<a className={this.props.disabled} onClick={this.onClick}>{this.props.tag.name}</a>
);
}
});
var PhotoDisplay = React.createClass({
getPhotoDetails: function (photo) {
console.log(this.props.displayedCategories, photo);
return (
<Photo title={photo.title} name={photo.name} tags={photo.tag} />
);
},
tagFilter: function (photo) {
return this.props.displayedCategories.length !== 0 &&
this.props.displayedCategories.every(function(thisTag) {
return photo.tag.some(function (photoTag) {
return photoTag.id === thisTag.id &&
photoTag.taglevel === thisTag.taglevel;
});
});
},
render: function () {
return (
<div>
{this.props.photoData.filter(this.tagFilter).map(this.getPhotoDetails)}
</div>
);
}
});
var Photo = React.createClass({
getTagDetail: function (tag){
return (
<li>{tag.name} ({tag.taglevel})</li>
);
},
sortTags: function (tagA, tagB) {
return tagA.taglevel - tagB.taglevel;
},
render: function(){
return (
<div className="photo-container" data-title={this.props.title} >
{this.props.title}
<ul>
{this.props.tags.sort(this.sortTags).map(this.getTagDetail)}
</ul>
</div>
);
}
});
ReactDOM.render(<PhotoGallery />, document.getElementById('main'));
With below react component I was able to do what you are looking for,
and here's what i've done in the code,
i) from the PHOTODATA array i have created taglevel1, taglevel2 array
one the render method at the begining.
ii) show them in two rows in showLevel1, showLevel2 function.
iii) when the tag item will be click it will call handleClick function and filter the data and save it to the filterData state.
import React, { Component } from 'react';
import { pluck } from 'underscore';
class Router extends Component {
constructor(props) {
super(props);
this.state = {
filterData: [],
};
this.filterArray = [];
this.PHOTODATA = [{
"title": "Into the Wild",
"tag": [
{
"name": "Movie",
"taglevel": 1,
"id": 1
},
{
"name": "Adventure",
"taglevel": 2,
"id": 30
},
{
"name": "Book",
"taglevel": 1,
"id": 2
}
],
"info": []
},{
"title": "Karate Kid",
"tag": [
{
"name": "Movie",
"taglevel": 1,
"id": 1
},
{
"name": "Adventure",
"taglevel": 2,
"id": 30
},
{
"name": "Kids",
"taglevel": 3,
"id": 4
}
],
"info": []
},
{
"title": "The Alchemist",
"tag": [
{
"name": "Book",
"taglevel": 1,
"id": 2
},
{
"name": "Adventure",
"taglevel": 2,
"id": 30
},
{
"name": "Classic",
"taglevel": 2,
"id": 4
},
{
"name": "Words",
"taglevel": 4,
"id": 4
}
],
"info": []
}];
}
handleClick(item) {
const findItem = this.filterArray.indexOf(item);
if (findItem === -1) {
this.filterArray.push(item);
} else {
this.filterArray.pop(item);
}
const filterData = [];
if(this.filterArray.length) {
this.PHOTODATA.map((item) => {
const data = pluck(item.tag, 'name');
let count = 0;
// console.log(data);
this.filterArray.map((filterItem) => {
const find = data.indexOf(filterItem);
if(find !== -1) {
count++;
}
});
if(count === this.filterArray.length) {
filterData.push(item);
}
});
}
console.log(this.filterArray);
this.setState({ filterData });
}
render() {
const taglevel1 = [];
const taglevel2 = [];
this.PHOTODATA.map((item) => {
item.tag.map((tagItem) => {
if(tagItem.taglevel === 1) {
const find = taglevel1.indexOf(tagItem.name);
if(find === -1) {
taglevel1.push(tagItem.name);
}
} else {
const find = taglevel2.indexOf(tagItem.name);
if(find === -1) {
taglevel2.push(tagItem.name);
}
}
});
});
const showLevel1 = (item, index) => {
return <span onClick={this.handleClick.bind(this, item)}> {item} </span>
};
const showLevel2 = (item, index) => {
return <span onClick={this.handleClick.bind(this, item)}> {item} </span>
};
const showData = (item, index) => {
return <div>{item.title}</div>
};
return (<div>
<ul>Tag Level 1: {taglevel1.map(showLevel1)}</ul>
<ul>Tag Level 2: {taglevel2.map(showLevel2)}</ul>
<div>Movie Title: {this.state.filterData.map(showData)}</div>
</div>);
}}
and here you can see how my outputs look like

Categories

Resources