react-pdf: how to view pdf and return to sender - react-pdf

I have successfully implemented react-pdf. The pdf has 3 navigation
buttons - Return, Previous, and Next. The button that does not work
is "Return". The functioning components are Send.js and RenderPDF.js.
Send.js displays a list of documents with an "Invoice" button adjacent
to each item. Clicking Invoice sends a prop (pdf name) to
RenderPDF.js, which renders the pdf. I have no graceful means of
returning to Send.js after viewing the pdf. Using history.push(), I
can return to any other page in my site except Send.js. Any
suggestions?
send.js
import React, {
useContext,
useEffect,
useState
} from 'react';
import PageTitle from '../components/common/PageTitle';
import SendPageHeading from '../components/common/SendPageHeading';
import {
FetchContext
} from '../context/FetchContext';
import ButtonDanger from '../components/common/ButtonDanger';
import ButtonLookup from '../components/common/ButtonLookUp';
import FormError from '../components/FormError';
import {
RenderPDF
} from '../components/common/RenderPDF.js';
const SendItemContainer = ({
children
}) => ( <
div className = "bg-white rounded shadow-md mb-2 p-0" > {
children
} < /div>
);
const SendItem = ({
item,
handlePdfProcess,
onDelete
}) => {
return ( <
div className = "flex" >
<
div className = "flex flex-row w-full" >
<
div className = "flex flex-row w-full my-1 " >
<
div className = "relative text-sm w-8 left-0 text-gray-600" > {
item.companyKey
} <
/div> <
div className = "relative text-sm w-88 left-50 text-gray-600" > {
item.companyName
} <
/div> <
div className = "relative text-sm w-8 left-100 text-gray-600" > {
item.docType
} <
/div> <
div className = "relative text-sm w-8 left-125 text-gray-600" > {
item.docNo
} <
/div> <
div className = "relative text-sm w-24 left-175 text-gray-600" > {
item.docDate.substr(5, 2) +
'-' +
item.docDate.substr(8, 2) +
'-' +
item.docDate.substr(0, 4)
} <
/div> <
div className = "relative text-sm w-94 left-225"
style = {
item.emailTo.indexOf(' ') === -1 ?
{
color: '#212529'
} :
{
color: '#e53e3e',
fontWeight: 'bold'
}
} >
{
item.emailTo
} <
/div> <
div className = "relative text-sm w-8 left-275 text-gray-600" > {
item.emailStatus
} <
/div> <
/div> <
div className = "flex flex-row self-end mr-3" >
<
ButtonLookup text = "Invoice"
onClick = {
() => handlePdfProcess(item)
}
/> <
ButtonDanger text = "Delete"
onClick = {
() => onDelete(item)
}
/> <
/div> <
/div> <
/div>
);
};
const Send = () => {
const fetchContext = useContext(FetchContext);
const [docData, setDocData] = useState([]);
const [pdfData, setPdfData] = useState();
const [pdfFlag, setPdfFlag] = useState(false);
const [errorMessage, setErrorMessage] = useState();
useEffect(() => {
const getSend = async () => {
try {
const {
data
} = await fetchContext.authAxios.get('send');
setDocData(data);
} catch (err) {
console.log('the err', err);
}
};
getSend();
}, [fetchContext]);
const onDelete = async item => {
try {
if (window.confirm('Are you sure you want to delete this item?')) {
const {
data
} = await fetchContext.authAxios.delete(
`docData/${item.id}`
);
setDocData(docData.filter(item => item.id !== data.modifiedItem.id));
}
} catch (err) {
const {
data
} = err.response;
setErrorMessage(data.message);
}
};
const handlePdfProcess = item => {
// const docName = item.docName;
setPdfData(`${item.docName}`);
setPdfFlag(true);
console.log(`🌵 pdfFlag: ${pdfFlag}`);
};
return pdfFlag ? ( <
>
<
RenderPDF dataFromSend = {
pdfData
}
/> <
/>
) : ( <
>
<
PageTitle title = "Invoice Staging" / >
<
SendPageHeading / > {
errorMessage && < FormError text = {
errorMessage
}
/>} {
docData && docData.length ?
docData.map(item => ( <
SendItemContainer key = {
item.id
} >
<
SendItem item = {
item
}
handlePdfProcess = {
handlePdfProcess
}
/> <
SendItem item = {
item
}
onDelete = {
onDelete
}
/> <
/SendItemContainer>
)) :
'No Document Items'
} <
/>
);
};
export default Send;
< RenderPDF.js
import React, {
useState
} from 'react';
import {
Document,
Page,
pdfjs
} from 'react-pdf/dist/esm/entry.webpack';
import {
useHistory
} from 'react-router-dom';
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
export const RenderPDF = dataFromSend => {
const [numPages, setNumPages] = useState(null);
const [pageNumber, setPageNumber] = useState(1); //setting 1 to show fisrt page
const history = useHistory(null);
function onDocumentLoadSuccess({
numPages
}) {
setNumPages(numPages);
setPageNumber(1);
}
const findValueByPrefix = (object, prefix) => {
for (var property in object) {
if (
object.hasOwnProperty(property) &&
property.toString().startsWith(prefix)
) {
return object[property];
}
}
};
let pdfDoc = findValueByPrefix(dataFromSend, 'dataFromSend');
pdfDoc = `/pdfs/${pdfDoc}`;
console.log(`🔥 prop: ${pdfDoc}`);
function changePage(offset) {
setPageNumber(prevPageNumber => prevPageNumber + offset);
}
function previousPage() {
changePage(-1);
}
function nextPage() {
changePage(1);
}
const return2Send = () => {
let path = '/send';
history.push(path);
};
return ( <
>
<
Document file = {
pdfDoc
}
onLoadSuccess = {
onDocumentLoadSuccess
} >
<
Page pageNumber = {
pageNumber
}
/> <
/Document> <
div >
<
p >
Page {
pageNumber || (numPages ? 1 : '--')
}
of {
numPages || '--'
} <
/p> <
button type = "button"
style = {
{
marginRight: '24px'
}
}
onClick = {
return2Send
} >
Return <
/button> <
button type = "button"
disabled = {
pageNumber <= 1
}
style = {
{
marginRight: '24px'
}
}
onClick = {
previousPage
} >
Previous <
/button> <
button type = "button"
disabled = {
pageNumber >= numPages
}
onClick = {
nextPage
} >
Next <
/button> <
/div> <
/>
);
};

Related

Unable to replace default image with image url that returns 404 in Map function in next.js

I want to display default image instead of white space
Swatch Image
for example https://cdn11.bigcommerce.com/s-hmhnh4h9/images/stencil/70w/attribute_value_images/252766.preview.jpg this return 404
while this returns 200 https://cdn11.bigcommerce.com/s-hmhnh4h9/images/stencil/70w/attribute_value_images/252738.preview.jpg
I even tried fetch method but still it has same result
I have built a function to detect if image loads or fail
`
async function getImageOrFallback(url: string, fallback: string) {
return new Promise((resolve, reject) => {
const img = new Image()
img.src = url
img.onload = () => resolve(url)
img.onerror = () => resolve(fallback);
}).catch(() => {
return fallback
})
}
`
Under swatch if image url is invalid then a default image needed to be displayed
`
<div className="option-section">
<div id="alloptions" className="flex_div flex justify-between flex-wrap flex-row py-4">
<input id="selected_variant" type="hidden" value={currentVariant.id} />
{variants.data.sort((a: any, b: any) => a.option_values[0].label.replace(/[^A-Za-z]/g, '').localeCompare(b.option_values[0].label.replace(/[^A-Za-z]/g, ''))).sort((a:any,b:any)=>{
if(a.option_values[0].label.replace(/[^A-Za-z]/g, '')[0] == b.option_values[0].label.replace(/[^A-Za-z]/g, '')[0]){
return a.option_values[0].label.replace(/[^0-9]/g, '').localeCompare(b.option_values[0].label.replace(/[^0-9]/g, ''));
}
}).sort((a:any,b:any) => {
if(variantCount < 8){
if (a.inventory_level === 0) {
return 1;
}
if (b.inventory_level === 0) {
return -1;
}
if (a.inventory_level === b.inventory_level) {
return 0;
}
return a < b ? -1 : 1;}else{
return false;
}
}).map((v: any, i: number) => {
let pUrl = product.path.replace('.html', '')
const opt = v.option_values[0]
const active = selectedOptions[opt.option_display_name.toLowerCase()]
// const swatch = v.image_url;
const swatch =
publicRuntimeConfig.COLOR_SWATCH_URL +
'/stencil/70w/attribute_value_images/' +
opt.id +
'.preview.jpg'
var imageUrl = getImageOrFallback(`${swatch}`, '/product-img-placeholder.svg').then(validUrl => {
validUrl
})
console.log("!!!!!!!!!!!!!!!! ", imageUrl)
const isDisabled = v.inventory_level == 0 ? ' color_disabled' : ' color_enabled'
const itemQty = items ? items[v.id] : 0
return (
<Swatch
key={`${opt.id}-${i}`}
active={v.sku}
variant={opt.option_display_name}
color={v.hexColors ? v.hexColors[0] : ''}
image={swatch ? swatch : ''}
label={opt.label}
id={itemQty}
className={customclass + isDisabled}
onClick={() => {
loadOptions(v);
setCurrentVariant(event, v, null);
}}
/>
)
})}
</div>
</div>
enter image description here
Under the Swatch component default image needed to display

How to access the div element with a key in React JS? (react-dnd)

This is a small app created using react-beautiful-dnd. I want to add individual css element for different key values passed through this div element. However I am unable to access this specific div element for the specific key value in css.
<div key={key} className={"column"}>
<ProjectWrapper className="border">
<hr className="hr"></hr>
<h3 className="title">{data.title}</h3>
</ProjectWrapper>
The entire source code:
import React, {useState} from 'react';
import './App.css';
import {DragDropContext, Droppable, Draggable} from "react-beautiful-dnd";
import _ from "lodash";
import {v4} from "uuid";
const item = {
id: v4(),
name: "Clean the house"
}
const item2 = {
id: v4(),
name: "Wash the car"
}
function App() {
const [text, setText] = useState("")
const [state, setState] = useState({
//creating 3 columns
"todo": {
title: "Todo",
items: [item, item2]//temporary data valuesa
},
"in-progress": {
title: "In Progress",
items: []
},
"done": {
title: "Completed",
items: []
}
})
const handleDragEnd = ({destination, source}) => {
if (!destination) {
return
}
if (destination.index === source.index && destination.droppableId === source.droppableId) {
return
}
// Creating a copy of item before removing it from state
const itemCopy = {...state[source.droppableId].items[source.index]}
setState(prev => {
prev = {...prev}
// Remove from previous items array
prev[source.droppableId].items.splice(source.index, 1)
// Adding to new items array location
prev[destination.droppableId].items.splice(destination.index, 0, itemCopy)
return prev
})
}
const addItem = () => {
setState(prev => {
return {
...prev,
todo: {
title: "Todo",
items: [
{
id: v4(),
name: text
},
...prev.todo.items
]
}
}
})
setText("")
}
//the dragdropcontext is the wrapper for draging elements
//dropable is the certain area inside the column where u can drop items
//dragble are the items in the column to be dragged
//here {_.map(state, (data, key) => {
//the above function is used to return the data and key of all 3 elements mentioned under use state() above
//the key: returns todo,inprogress,and done
//where as the data returns all the values of the variables within each key
return (
<div className="App">
<div>
<input type="text" value={text} onChange={(e) => setText(e.target.value)}/>
<button onClick={addItem}>Add</button>
</div>
<DragDropContext onDragEnd={handleDragEnd}>
{_.map(state, (data, key) => {
return(
<div key={key} className={"column"}>
<h3>{data.title}</h3>
<Droppable droppableId={key}>
{(provided, snapshot) => {
return(
<div
ref={provided.innerRef}
{...provided.droppableProps}
key = {"Todo"}
className={"droppable-col"}
>
{data.items.map((el, index) => {
return(
<Draggable key={el.id} index={index} draggableId={el.id}>
{(provided, snapshot) => {
console.log(snapshot)
return(
<div
className={`item ${snapshot.isDragging && "dragging"}`}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{el.name}
</div>
)
}}
</Draggable>
)
})}
{provided.placeholder}
</div>
)
}}
</Droppable>
</div>
)
})}
</DragDropContext>
</div>
);
}
export default App;
May I suggest using a utility like clsx
It will apply the specific class based on your value like so:
import clsx from 'clsx';
export default function App() {
const key = 'todo'; // or 'in-progress' or 'done'
return (
<div className={clsx('existingClass', key)}>
.
.
.
</div>
);
}
Which renders as <div class="existingClass todo">...</div>. You can configure your CSS classnames for the dynamic values. (in this case, todo)
// https://github.com/lukeed/clsx/blob/master/src/index.js
// MIT © Luke Edwards
function toVal(mix) {
var k, y, str='';
if (typeof mix === 'string' || typeof mix === 'number') {
str += mix;
} else if (typeof mix === 'object') {
if (Array.isArray(mix)) {
for (k=0; k < mix.length; k++) {
if (mix[k]) {
if (y = toVal(mix[k])) {
str && (str += ' ');
str += y;
}
}
}
} else {
for (k in mix) {
if (mix[k]) {
str && (str += ' ');
str += k;
}
}
}
}
return str;
}
function clsx() {
var i=0, tmp, x, str='';
while (i < arguments.length) {
if (tmp = arguments[i++]) {
if (x = toVal(tmp)) {
str && (str += ' ');
str += x
}
}
}
return str;
}
// https://github.com/lukeed/clsx/blob/master/src/index.js
// MIT © Luke Edwards
//-------------------
function App() {
const key = ['todo', 'in-progress', 'done'];
return (
<div className="App">
{key.map((k) => (
<span className={clsx('text', k)}>{k} - </span>
))}
</div>
);
}
ReactDOM.render(<App />, document.getElementById("react"));
.App {
font-family: sans-serif;
text-align: center;
}
.text{
font-size: 16px;
margin: 12px;
}
.todo {
color: orange;
}
.in-progress {
color: magenta;
}
.done {
color: green;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Vue 3 Composition API broken after re-rendering the component

thanks for reading my post.
It's hard to describe my issue.
I have a reactive object cached globally(like a store and let's call it 'store' here) to preserve the states together with some actions. It's working all good initially. But when I added more pages & switching between the pages, the components are broken after re-rendering. The components can still read correctly from the store and call the actions to update the store data and can console.log the 'fingerprint' to show it's the same store. But the updated data in store is no longer updated in the UI. I can console.log the data in store which looks all good, but the UI is frozen with the data since last unmounted.
I'm providing my component code below.
<template>
<div class="min-w-full relative" ref="container">
<hi-transition slow>
<center-box
v-if="loading"
class="absolute w-full h-full left-0 top-0 text-primary-light opacity-50"
>
<hi-spinner class="w-1/6 h-1/6" :stroke="2" />LOADING...
</center-box>
<div v-else-if="noResult">
No Result
</div>
<hi-markable
v-else
class="w-full h-full divide-y divide-primary-lighter overflow-y-auto block"
:search="searchValue"
>
<div
v-for="item in displayItems"
:key="item.id"
class="hover:bg-primary-lightest transition-colors duration-500 flex items-center block"
#click="selectItem(item)"
:style="{
'min-height': minItemHeight + 'px',
height: itemHeight + 'px'
}"
:class="{ active: currentItem === item }"
>
<slot :item="item" />
</div>
</hi-markable>
</hi-transition>
</div>
</template>
<script>
import { inject, ref } from "vue";
import HiSpinner from "#/ui/HiSpinner";
import CenterBox from "#/ui/elements/CenterBox";
import HiTransition from "#/ui/HiTransition";
import HiMarkable from "#/ui/HiMarkable";
import { computed } from "vue";
export default {
name: "HiDataList",
components: { HiMarkable, HiTransition, CenterBox, HiSpinner },
props: {
storeToken: String,
minItemHeight: [Number, String],
autoItemsPerPage: Boolean
},
emits: ["select"],
setup(props, { emit }) {
//this store is a reactive object cached somewhere
const store = inject(props.storeToken);
//to make sure it's the same store
console.log("fingerprint", store.fingerprint);
const { displayItems, currentItem, loading, searchValue, noResult } = store;
const container = ref(null);
const itemHeight = computed(() => {
return props.autoItemsPerPage
? store.autoItemHeight.value
: props.minItemHeight;
});
if (props.autoItemsPerPage) {
store.autoItemsPerPage(container, props.minItemHeight);
}
function selectItem(item) {
console.log(item);
emit("select", item);
store.setCurrentItem(item);
}
return {
displayItems,
selectItem,
container,
itemHeight,
currentItem,
loading,
searchValue,
noResult
};
}
};
</script>
<style scoped>
.active,
.active:hover {
#apply bg-primary-lighter border-primary-light;
}
</style>
The listStore
import { computed, reactive, watch, toRefs } from "vue";
import { useElementSize } from "#vueuse/core";
import { watchProps } from "#/utils/reactiveHelpers";
function filter(item, filters) {
const f = (key, filter) => {
const itemVal = item[key];
if (Array.isArray(itemVal)) return itemVal.indexOf(filter) >= 0;
else return itemVal === filter;
};
for (let key in filters) {
const filterVal = filters[key];
if (Array.isArray(filterVal)) {
for (let i = 0; i < filterVal.length; i++) {
if (f(key, filterVal[i])) return true;
}
} else {
return f(key, filterVal);
}
}
return false;
}
const getTime = date => {
if (date.milliseconds) return date.milliseconds;
if (date.seconds) return date.seconds;
if (date.getTime) return date.getTime();
};
const createListStore = (source, settings = {}) => {
const state = reactive({
source: source,
currentPage: 0,
itemsPerPage: 0, //zero means display all
loading: true,
ready: false,
noData: false,
noResult: false,
filters: {},
search: null,
searchables: settings.searchables || [],
sortBy: null,
sortType: "alpha",
desc: false,
currentItem: null,
autoItemHeight: 0,
fingerprint: Math.random() * 10000000000000000
});
// const { itemsPerPage,source,filters,search,searchables,sortBy,sortType,desc } = toRefs(state);
const {
itemsPerPage,
ready,
loading,
noResult,
search,
autoItemHeight
} = toRefs(state);
watchProps(state, "source", v => {
if (typeof v !== "undefined") {
state.ready = true;
state.loading = false;
if (!v.length) state.noData = true;
}
});
const currentPage = computed(() => state.currentPage + 1);
// const itemsPerPage = computed(() => state.itemsPerPage);
const totalItems = computed(() => results.asc.length);
const from = computed(() => {
if (totalItems.value === 0) return 0;
return state.currentPage * state.itemsPerPage + 1;
});
const to = computed(() => {
const t = from.value + displayItems.value.length - 1;
return t > totalItems.value ? totalItems.value : t;
});
const totalPages = computed(() => {
if (totalItems.value === 0 || state.itemsPerPage === 0) return 1;
return Math.ceil(totalItems.value / state.itemsPerPage);
});
const gotoPage = page => {
console.log("gotoPage", page);
state.currentPage = page - 1;
console.log(state.currentPage);
};
const prevPage = () => {
if (state.currentPage > 0) state.currentPage--;
};
const nextPage = () => {
if (state.currentPage < totalPages.value) state.currentPage++;
};
const updateFilters = (filter, val) => {
state.filters[filter] = val;
};
/**
*
* #param column
* #param desc
* #param type "alpha"|"number"|"date"|"time"
*/
const sortBy = (column, desc = false, type = "alpha") => {
state.sortBy = column;
state.desc = desc;
state.sortType = type;
};
function doSearch(item) {
const searchables = state.searchables;
for (let i = 0; i < searchables.length; i++) {
const key = searchables[i];
let value = item[key];
if (value && typeof value === "string") {
value = value.toLowerCase();
}
if (value && value.indexOf(state.search) >= 0) {
return true;
}
}
return false;
}
const results = reactive({
desc: [],
asc: []
});
function calcResults() {
if (!state.ready || state.noData) return null;
// console.log("re-calc results....");
const hasFilters = Object.keys(state.filters).length > 0;
// console.log(Object.keys(state.filters));
let items = [];
if (hasFilters || (state.search && state.search.length)) {
//do filter & search
const source = state.source;
for (let i = 0; i < source.length; i++) {
const item = source[i];
// console.log(filter(item, state.filters));
if (hasFilters && !filter(item, state.filters)) {
continue;
}
if (state.search && state.search.length && !doSearch(item)) {
continue;
}
items.push(item);
}
if (!items.length) {
results.desc = results.asc = [];
return null;
}
} else {
items = state.source;
}
if (state.sortBy) {
//do sort
const sort = state.sortBy;
// const desc = state.desc ? -1 : 1;
const type = state.sortType.toLowerCase();
items.sort((a, b) => {
a = a[sort];
b = b[sort];
if (type === "date" || type === "time") {
return getTime(a) - getTime(b);
} else {
if (typeof a === "string") a = a.trim();
if (typeof b === "string") b = b.trim();
if (state.sortType.toLowerCase() === "number") {
return a - b;
} else {
return a.localeCompare(b, "en", { sensitivity: "base" });
}
}
});
}
results.asc = items;
results.desc = [...items].reverse();
// return items;
}
//changed to watch for the wired vue error.
watchProps(
state,
["source", "filters", "search", "searchables", "sortBy", "sortType"],
() => {
calcResults();
state.noResult = results.asc.length === 0;
}
);
const displayItems = computed(() => {
if (!results.asc.length) return [];
const re = state.desc ? results.desc : results.asc;
if (state.itemsPerPage === 0) return re;
const from = state.currentPage * state.itemsPerPage;
const to = from + state.itemsPerPage;
return re.slice(from, to);
});
/**
*
* #param elementRef ref
* #param minHeight Number
* #param itemHeightRef ref
*/
const autoItemsPerPage = (elementRef, minHeight) => {
const { height } = useElementSize(elementRef);
// console.log(elementRef);
watch(height, v => {
const items = Math.floor(v / minHeight);
// itemHeightRef.value = v / items;
// console.log(v / items);
state.itemsPerPage = items;
state.autoItemHeight = v / items;
});
};
const setCurrentItem = item => {
console.log("set current", state.fingerprint);
state.currentItem = item;
};
const currentItem = computed(() => state.currentItem);
return {
currentPage,
itemsPerPage,
totalItems,
displayItems,
from,
to,
totalPages,
gotoPage,
prevPage,
nextPage,
ready,
loading,
updateFilters,
sortBy,
search: v => (state.search = v),
searchValue: search,
autoItemsPerPage,
setCurrentItem,
currentItem,
noResult,
autoItemHeight,
fingerprint: state.fingerprint
};
};
export { createListStore };
The store provider
import { provide } from "vue";
import { createListStore } from "#/ui/storeProvider/listStore";
const storeDepot = {};
export const provideListStore = (token, source, settings = {}) => {
if (!storeDepot[token]) {
console.log("create new store", token);
storeDepot[token] = createListStore(source, settings);
}
provide(token, storeDepot[token]);
return storeDepot[token];
};

Reset Box in Tic Tac Toe with React

I am having issue with resetting the game whenever I click on reset button.
I have created tic tac toe game with react hooks with 2 components Game and
Box.How can I add reset button with the use of React hooks.
I am Trying to make a with the code below.Also I tried to add reset button in it but is not working as required.Please help me out on how I can use reset button correctly to reset the box to empty.
Here is my Box component.
import React, { useState } from 'react'
import './style.css'
function Box(props) {
const [text, setText] = useState('')
function toggleText() {
if(text === '') {
setText(props.currentState)
props.changeTurn(props.row, props.col)
}
}
return <div className="box" onClick={toggleText}>{text}</div>
}
export default Box
Here is my Game Component.
import './style.css'
import Box from '../Box'
const board = [[],[],[]]
function Game() {
const [turn, setTurn] = useState('X')
const [winningtext, setWinningText] = useState('')
console.log(board)
function changeTurn(row, col) {
board[row][col] = turn
setTurn(turn => turn === 'X' ? 'O' : 'X' )
//console.log(board.length)
const winner = checkForWin()
//console.log(winner)
if(winner) {
setWinningText(winner + ' Won!')
}
}
// Winning game logic
function checkForWin() {
// row test
for(let i = 0; i < board.length; i++) {
const row = board[i]
//console.log(row[0])
if(row[0] === row[1] && row[1] === row[2] && row[0]){
return row[0]
}
}
//column test
for(let i = 0; i < board.length; i++) {
const el1 = board[0][i]
const el2 = board[1][i]
const el3 = board[2][i]
//console.log(`${el1} ${el2} ${el3}`)
if(el1 === el2 && el2 === el3 && el1) {
return el1
}
}
//diagonal test
const d1 = board[0][0]
const d2 = board[1][1]
const d3 = board[2][2]
if(d1 === d2 && d2 === d3 && d1) {
return d1
}
const p1 = board[0][2]
const p2 = board[1][1]
const p3 = board[2][0]
if(p1 === p2 && p2 === p3 && p1) {
return p1
}
return false
}
function reset() {
//Clear all grids and winner message
setWinningText('')
}
return <div className="game">
<h1>Tic Tac Toe</h1><br/>
<button className="reset" onClick={reset}>Reset</button><br/><br/>
<div id="winning-text">{winningtext}</div>
<div className="row row-1">
<Box row={0} col={0} currentState={turn} changeTurn={changeTurn} />
<Box row={0} col={1} currentState={turn} changeTurn={changeTurn} />
<Box row={0} col={2} currentState={turn} changeTurn={changeTurn} />
</div>
<div className="row row-2">
<Box row={1} col={0} currentState={turn} changeTurn={changeTurn} />
<Box row={1} col={1} currentState={turn} changeTurn={changeTurn} />
<Box row={1} col={2} currentState={turn} changeTurn={changeTurn} />
</div>
<div className="row row-3">
<Box row={2} col={0} currentState={turn} changeTurn={changeTurn} />
<Box row={2} col={1} currentState={turn} changeTurn={changeTurn} />
<Box row={2} col={2} currentState={turn} changeTurn={changeTurn} />
</div>
</div>
}
export default Game
In general, if there is "state" (eg: boards) you want that to be within a React component. You could reset boards by setting it to the same initial value (eg: [[], [], []]) but it will not re-render because React won't know it changed. The suggestion I offered below is to move boards to use the useState hook so whenever it changes React can properly re-render.
Another issue is that both board and the local value in the Box component were storing similar state so it would be a challenge to keep them in sync. Rather, having a single board value in the parent and passing a callback to the Box will probably make your life easier and avoid weird data synchronization issues.
You didn't share the CSS so I added some basic styles. Consider using CSS grid for this (seems like a perfect use case).
Consider using a map for each row and column to reduce the need for the duplication (this is simple enough it's not a big deal, but with this, you could easily make it a 5 x 5 grid or something).
I made as few changes as possible to get it working, so might have missed some things.
function Box(props) {
const { value, changeTurn, row, col } = props;
function toggleText() {
if (!value) {
changeTurn(row, col);
}
}
return (
<div className="box" onClick={toggleText}>
{value}
</div>
);
}
function Game() {
const [board, setBoard] = React.useState([[], [], []]);
const [turn, setTurn] = React.useState("X");
const [winningtext, setWinningText] = React.useState("");
console.log(board);
function changeTurn(row, col) {
const newBoard = [...board];
const newRow = [...board[row]];
newBoard[row] = newRow;
newBoard[row][col] = turn;
setBoard(newBoard);
setTurn(turn => (turn === "X" ? "O" : "X"));
//console.log(board.length)
const winner = checkForWin();
//console.log(winner)
if (winner) {
setWinningText(winner + " Won!");
}
}
// Winning game logic
function checkForWin() {
// row test
for (let i = 0; i < board.length; i++) {
const row = board[i];
//console.log(row[0])
if (row[0] === row[1] && row[1] === row[2] && row[0]) {
return row[0];
}
}
//column test
for (let i = 0; i < board.length; i++) {
const el1 = board[0][i];
const el2 = board[1][i];
const el3 = board[2][i];
//console.log(`${el1} ${el2} ${el3}`)
if (el1 === el2 && el2 === el3 && el1) {
return el1;
}
}
//diagonal test
const d1 = board[0][0];
const d2 = board[1][1];
const d3 = board[2][2];
if (d1 === d2 && d2 === d3 && d1) {
return d1;
}
const p1 = board[0][2];
const p2 = board[1][1];
const p3 = board[2][0];
if (p1 === p2 && p2 === p3 && p1) {
return p1;
}
return false;
}
function reset() {
setBoard([[], [], []]);
setWinningText("");
}
return (
<div className="game">
<h1>Tic Tac Toe</h1>
<br />
<button className="reset" onClick={reset}>
Reset
</button>
<br />
<br />
<div id="winning-text">{winningtext}</div>
<div className="boxes">
{[0, 1, 2].map(row => {
return [0, 1, 2].map(col => {
return (
<Box
row={row}
col={col}
key={`${row}-${col}`}
value={board[row][col]}
currentState={turn}
changeTurn={changeTurn}
/>
);
});
})}
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Game />, rootElement);
.boxes {
display: grid;
grid-template-columns: 32px 32px 32px;
}
.box {
border: 1px solid red;
width: 32px;
height: 32px;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
You could make the box component stateless, so you move the state logic from each box component out of it into the game component instead. You already have an array for the whole board, so why not just pass the state from the board to each box instead? Then you can also reset the board array, which would reset all boxes.
Check the following example. This hasn't been tested, but think it would work.
import React from 'react'
import './style.css'
function Box(props) {
function onClick() {
props.changeTurn(props.row, props.col)
}
return <div className="box" onClick={onClick}>{props.currentState}</div>
}
export default Box
import './style.css'
import Box from '../Box'
const board = [[],[],[]];
function Game() {
const [turn, setTurn] = useState('X')
const [winningtext, setWinningText] = useState('')
console.log(board)
function changeTurn(row, col) {
board[row][col] = turn
setTurn(turn => turn === 'X' ? 'O' : 'X' )
//console.log(board.length)
const winner = checkForWin()
//console.log(winner)
if(winner) {
setWinningText(winner + ' Won!')
}
}
// Winning game logic
function checkForWin() {
// row test
for(let i = 0; i < board.length; i++) {
const row = board[i]
//console.log(row[0])
if(row[0] === row[1] && row[1] === row[2] && row[0]){
return row[0]
}
}
//column test
for(let i = 0; i < board.length; i++) {
const el1 = board[0][i]
const el2 = board[1][i]
const el3 = board[2][i]
//console.log(`${el1} ${el2} ${el3}`)
if(el1 === el2 && el2 === el3 && el1) {
return el1
}
}
//diagonal test
const d1 = board[0][0]
const d2 = board[1][1]
const d3 = board[2][2]
if(d1 === d2 && d2 === d3 && d1) {
return d1
}
const p1 = board[0][2]
const p2 = board[1][1]
const p3 = board[2][0]
if(p1 === p2 && p2 === p3 && p1) {
return p1
}
return false
}
function reset() {
//Clear all grids and winner message
const board = [[],[],[]];
}
return <div className="game">
<h1>Tic Tac Toe</h1><br/>
<button className="reset" onClick={reset}>Reset</button><br/><br/>
<div id="winning-text">{winningtext}</div>
<div className="row row-1">
<Box row={0} col={0} currentState={board[0][0]} changeTurn={changeTurn} />
<Box row={0} col={1} currentState={board[0][1]} changeTurn={changeTurn} />
<Box row={0} col={2} currentState={board[0][2]} changeTurn={changeTurn} />
</div>
<div className="row row-2">
<Box row={1} col={0} currentState={board[1][0]} changeTurn={changeTurn} />
<Box row={1} col={1} currentState={board[1][1]} changeTurn={changeTurn} />
<Box row={1} col={2} currentState={board[1][2]} changeTurn={changeTurn} />
</div>
<div className="row row-3">
<Box row={2} col={0} currentState={board[2][0]} changeTurn={changeTurn} />
<Box row={2} col={1} currentState={board[2][1]} changeTurn={changeTurn} />
<Box row={2} col={2} currentState={board[2][2]} changeTurn={changeTurn} />
</div>
</div>
}
export default Game

ReactJS+MaterialUI v1 automatic nested list padding

I have nested lists and I don't know how many (recursive function). How to make them to get automatic padding?
The result I get is this
:
And I want to get this result:
If I add nested styles, they are the same for all nested lists. I need the padding to increase for each next level.
Can you help? (All comments are welcome!)
The code:
import React from 'react';
import {isNullOrUndefined} from "util";
import {TagNode} from "../interfaces/TagTree";
import HistoryDataUtility from "../../utilities/historyDataUtility";
import TreeUtility from "../../utilities/treeUtility";
import { createStyles } from '#material-ui/core/styles';
import { withStyles } from '#material-ui/core/styles';
import Checkbox from '#material-ui/core/Checkbox';
import Collapse from '#material-ui/core/Collapse';
import Icon from '#material-ui/core/Icon';
import List from '#material-ui/core/List';
import ListItem from '#material-ui/core/ListItem';
import ListItemIcon from '#material-ui/core/ListItemIcon';
import ListItemSecondaryAction from '#material-ui/core/ListItemSecondaryAction';
import ListItemText from '#material-ui/core/ListItemText';
export interface TreeRendererProps {
itemList: TagNode[],
selectedTags: string[],
expandedFolders: string[],
onFolderToggle: any,
onTagToggle: any,
onSelectedFolderChange?: any,
selectedFolder?: string,
classes: {
root: string;
nested: string;
}
}
const styles = createStyles({
root: {
width: '100%',
},
nested: {
paddingLeft: 16
}
});
const TreeRenderer: React.StatelessComponent<TreeRendererProps> = (props) => {
const buildListItems = (item: TagNode,
selectedTags: string[],
onFolderToggle: any,
onTagToggle: any,
source: string) => {
let key = HistoryDataUtility.combineTag(source, item.objId);
let isExpanded = props.expandedFolders.indexOf(key) != -1;
let isSelected: boolean = props.selectedFolder === key ? true : false;
let children1: any, children2: any;
if(!TreeUtility.isTagItem(item)) {
children1 = item.children.filter(filterGroups).sort(sortOnTitle).map((child) => {
return buildListItems(child,
selectedTags,
onFolderToggle,
onTagToggle,
source);
}) || null;
children2 = item.children.filter(filterTags).sort(sortOnTitle).map((child) => {
return buildListItems(child,
selectedTags,
onFolderToggle,
onTagToggle,
source);
}) || null;
}
let collapseItem: any;
if(isExpanded && !isNullOrUndefined(item.children)) {
collapseItem =
<Collapse component="li" in={true} timeout="auto" unmountOnExit>
<List disablePadding>
{children1}
{children2}
</List>
</Collapse>
}
let key2 = "list" + Math.random().toString(36).substr(2, 9);
return (
!TreeUtility.isTagItem(item)
?
<div key={key2}>
<ListItem
button key={key}
className={props.classes.nested + " with-children"}
onClick={onFolderToggle.bind(this, key)}
>
<ListItemIcon>
{isExpanded ? <Icon>remove</Icon> : <Icon>add</Icon>}
</ListItemIcon>
<ListItemText primary={item.title} />
<ListItemSecondaryAction>
<Checkbox checked={isSelected} color="primary" onChange={props.onSelectedFolderChange} value={key} />
</ListItemSecondaryAction>
</ListItem>
{collapseItem}
</div>
:
<ListItem
button
className={props.classes.nested + " no-children" + ((selectedTags.indexOf(key) != -1 || selectedTags.indexOf(item.objId) != -1) ? " tagitem-activated" : "")}
key={key}
onClick={onTagToggle.bind(this, key)}
>
<ListItemIcon><Icon className="status">fiber_manual_record</Icon></ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
);
}
const filterGroups = (item: TagNode): boolean => {
return !TreeUtility.isTagItem(item);
}
const filterTags = (item: TagNode): boolean => {
return TreeUtility.isTagItem(item);
}
const sortOnTitle = (a: TagNode, b: TagNode) => {
var nameA = a.title.toUpperCase(); // ignore upper and lowercase
var nameB = b.title.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
}
const buildList = (items: TagNode[],
selectedTags: string[],
onFolderToggle: any,
onTagToggle: any) => {
let children1: any, children2: any;
children1 = items.filter(filterGroups).sort(sortOnTitle).map((item: TagNode) => {
let source = item.objId; //Use the top level group nodes id as source
return buildListItems(
item,
selectedTags,
onFolderToggle,
onTagToggle, source);
}) || null;
children2 = items.filter(filterTags).sort(sortOnTitle).map((item: TagNode) => {
return buildListItems(
item,
selectedTags,
onFolderToggle,
onTagToggle, null);
}) || null;
return (
<List>
{children1}
{children2}
</List>
)
}
let tree;
if (props.itemList.length > 0) {
if (props.itemList[0].children != undefined) {
tree = buildList(
props.itemList[0].children,
props.selectedTags,
props.onFolderToggle,
props.onTagToggle);
} else {
tree = <div>{props.itemList[0].name + ' : ' + props.itemList[0].title}</div>
}
} else {
tree = <div><h2 className="small">Model not found.</h2></div>;
}
return (<div>{tree}</div>);
}
export default withStyles(styles, { withTheme: true })(TreeRenderer);
Actually, the solution is amazingly simple. Just change this line:
<Collapse component="li" in={true} timeout="auto" unmountOnExit>
to this:
<Collapse component="li" in={true} timeout="auto" unmountOnExit style={{paddingLeft: '16px'}}>

Resources