How do I make a responsive master-detail layout with react-router v4 as described in 'philosophy' - react-router-v4

I am trying to make a responsive Master/Detail layout using react-router v4 as described here. The code it suggests is
const App = () => (
<AppLayout>
<Route path="/invoices" component={Invoices}/>
</AppLayout>
)
const Invoices = () => (
<Layout>
{/* always show the nav */}
<InvoicesNav/>
<Media query={PRETTY_SMALL}>
{screenIsSmall => screenIsSmall
// small screen has no redirect
? <Switch>
<Route exact path="/invoices/dashboard" component={Dashboard}/>
<Route path="/invoices/:id" component={Invoice}/>
</Switch>
// large screen does!
: <Switch>
<Route exact path="/invoices/dashboard" component={Dashboard}/>
<Route path="/invoices/:id" component={Invoice}/>
<Redirect from="/invoices" to="/invoices/dashboard"/>
</Switch>
}
</Media>
</Layout>
)
However I am unable to get something working based on this. There are a few things that I am not sure about:
1) What is this AppLayout component?
2) Which Layout component is it referring to and is this important?
3) I am assuming the Media tag refers to react-media?
4) Media query={PRETTY_SMALL} means something along the lines of Media query={{ maxWidth: 599 }}
5) There is no Router component anywhere, which I though was needed
6) There are no Link objects anywhere
The best I have come up with so far (on a project started with create-react-app) is
import Media from "react-media";
import React, { Component } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
const data = [
{
id: 1,
to: 'Mr. Smith',
amount: 10
},
{
id: 2,
to: 'Mrs. Jones',
amount: 100
}
]
const InvoicesNav = () => (
<div>
Nav Bar
</div>
)
const Dashboard = () => (
<div>
Dashboard
</div>
)
const Invoice = () => (
<div>
Invoice
</div>
)
const App = () => (
// <AppLayout>
<Route path="/invoices" component={Invoices} />
// </AppLayout>
)
const Invoices = () => (
// <Layout>
<div>
{/* always show the nav */}
<InvoicesNav />
<Media query={{ maxWidth: 599 }}>
{screenIsSmall => screenIsSmall
// small screen has no redirect
? <Switch>
<Route exact path="/invoices/dashboard" component={Dashboard} />
<Route path="/invoices/:id" component={Invoice} />
</Switch>
// large screen does!
: <Switch>
<Route exact path="/invoices/dashboard" component={Dashboard} />
<Route path="/invoices/:id" component={Invoice} />
<Redirect from="/invoices" to="/invoices/dashboard" />
</Switch>
}
</Media>
{/* </Layout> */}
</div>
)
export default App;
But this still doesn't really do anything that resembles a responsive Master/Detail layout and neither does anything in the samples for react-router as far as I can see. :(

That layout is just an idea of your app structure. They don't give much details about it hence you are free to use any content you want for unknown tags, vars, etc.
I couldn't exactly reproduce it because IMO <InvoicesNav /> shouldn't be always displayed because in the mobile view there is no place for it when you view Dashboard or an exact invoice. I assumed that <InvoicesNav /> is the left sidebar of invoices with their IDs.
I hope that you can find answers to your questions in this basic implementation of that philosophy github.com/vogdb/react-router-mediaquery-example. Also there is another very detailed example https://github.com/AWebOfBrown/React-MQL-Manager.
import Media from "react-media";
import React, {Component} from 'react';
import {Route, Switch, Link} from 'react-router-dom';
const InvoiceList = (props) => {
return <ul>
<li key="dashboard">
<Link to="/invoices/dashboard"><span>Dashboard</span></Link>
</li>
{props.invoices.map((invoice) =>
<li key={invoice.id}>
<p><Link to={`/invoices/${invoice.id}`}><span>To:</span>{invoice.to}</Link></p>
<p><span>Amount:</span>{invoice.amount}</p>
<p><span>Paid:</span>{invoice.paid ? 'Yes' : 'No'}</p>
<p><span>Due:</span>{invoice.due.toDateString()}</p>
</li>
)}
</ul>
};
const Dashboard = (props) => {
const balance = props.invoices.reduce((sum, invoice) => sum + invoice.amount, 0);
const unpaidNum = props.invoices.reduce((num, invoice) => num + !invoice.paid, 0);
return <div>
Dashboard:
<div>Unpaid: {unpaidNum}</div>
<div>Balance: {balance}</div>
</div>
};
const Invoice = (props) => {
const id = parseInt(props.match.params.id);
const invoice = props.invoices.find(invoice => invoice.id === id);
return <div>
Invoice #{id}, to: {invoice.to}
</div>
};
const Layout = (props) => (
<div className="invoicesLayout">
{props.children}
</div>
);
const InvoicesSmallScreen = (props) => {
const {invoices} = props;
return (<div>
<Link to="/invoices">Show Invoices</Link>
<Switch>
<Route exact path="/invoices" render={props => <InvoiceList invoices={invoices} {...props}/>}/>
<Route exact path="/invoices/dashboard" render={props => <Dashboard invoices={invoices} {...props}/>}/>
<Route path="/invoices/:id" render={props => <Invoice invoices={invoices} {...props}/>}/>
</Switch>
</div>)
};
const InvoicesBigScreen = (props) => {
const {invoices} = props;
return (<div>
<InvoiceList invoices={invoices}/>
<Switch>
<Route exact path="/invoices/dashboard" render={props => <Dashboard invoices={invoices} {...props}/>}/>
<Route path="/invoices/:id" render={props => <Invoice invoices={invoices} {...props}/>}/>
</Switch>
</div>)
};
class Invoices extends Component {
constructor(props) {
super(props);
this.state = {
invoices: []
}
}
componentDidMount() {
// generate some dummy data here
const data = [];
this.setState({invoices: data})
}
render() {
const {invoices} = this.state;
return (
<Layout>
<Media query={{maxWidth: 599}}>
{screenIsSmall => screenIsSmall ?
<InvoicesSmallScreen invoices={invoices}/>
: <InvoicesBigScreen invoices={invoices}/>
}
</Media>
</Layout>
)
}
}
export default Invoices;

I recently tried a similar thing, and ended up with the following higher order component, where you pass in a master and a detail component:
import React from 'react';
import { Route, Switch, useRouteMatch } from 'react-router-dom';
import Media from 'react-media';
import { mediaQueries } from 'model';
import './MasterDetail.scss';
export const masterDetailHOC = <X,Y>(
MasterComponent: any,
DetailComponent: any,
masterProps?: X, detailProps?: Y) => {
return function(props: any) {
let { path } = useRouteMatch() as any;
return (
<Media query={mediaQueries.md}>
{matches =>
matches ? (
<Switch>
<Route exact path={`${path}`}>
<MasterComponent {...props} {...masterProps}
data-test="Master" />
</Route>
<Route path={`${path}/detail/:id`}>
<DetailComponent {...props} {...detailProps}
data-test="Detail" />
</Route>
</Switch>
) : (
<section className="master-detail">
<section className="master-detail__master">
<Route path={`${path}`}>
<MasterComponent {...props} {...masterProps}
data-test="Master" />
</Route>
</section>
<section className="master-detail__detail">
<Switch>
<Route exact path={`${path}`}>
<DetailComponent {...detailProps}
data-test="Detail" />
</Route>
<Route path={`${path}/detail/:id`}>
<DetailComponent {...props} {...detailProps}
data-test="Detail" />
</Route>
</Switch>
</section>
</section>
)
}
</Media>
);
}
};
If you like, I wrote an accompanying blog post with more detail here + the code can be found on github.

Related

Material UI TextField select not showing initial value on form edit

I have an edit form in a modal that lets you edit user info. Here is the relevant portion:
export const EditUserModal = (Props) => {
const [formValues, setFormValues] = useState(Props.userData);
const [reporterData, setReporterData] = React.useState<any[]>([]);
React.useEffect(() => {
let apiClient = new APIClient();
apiClient.getOwners().then((response) => {
setReporterData(response);
});
}, []);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value,
});
};
return (
<>
<Modal
open={Props.show}
onClose={() => { Props.toggleModal(false)}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
component="form"
autoComplete="off"
>
<Typography id="modal-modal-title" variant="h4" component="h4">
Edit User {Props.userData.name}
</Typography>
<div>
<TextField
required
id="outlined-select-basic"
select
name="reports_to"
label="Reports To"
value={formValues.reports_to}
onChange={handleInputChange}
>
{reporterData.map((option) => (
<MenuItem key={option.owner} value={option.owner}>
{option.owner}
</MenuItem>
))}
</TextField>
</div>
</Box>
</Modal>
</>
)
};
The TextField is supposed to show the initial value of {formValues.reports_to}, but it remains blank (the value is still correct) until you make a new selection. reporterData just returns a list of names(string).
I have tried adding it as a defaultValue, which did not work.

How to update input defaultValue in React child component when props change?

I can pass props to the child component, but the input value doesn't change, when I update props (e.g. I send down "second" instead of "first", I can console.log "second" but the input value remains "first".) What could be the problem?
Code example:
// in parent component
const ParentComp = () => {
const [showEdit, setShowEdit] = useState(false);
const [currentElement, setCurrentElement] = useState('');
const myList = [{ label: 'first'}, {label: 'second'}]
const editElement = (el) => {
setShowEdit(true);
setCurrentElement(el);
}
return (
<div>
{myList.map((el, i) => (
<span key={i} onClick={() => editElement(el)}>
{el.label}
</span>
))}
{showEdit && (
<ChildComponent elData={currentElement} />
)}
</div>
)}
// in child component
const ChildComponent = ({ elData }) => {
const [testInput, setTestInput] = useState(elData.label)
return (
<input
onChange={(e) => setTestInput(e.target?.value)}
defaultValue={elData.label}
/>
)
}
I found a working solution but I'm not quite sure how it works.
// old code
<input
onChange={(e) => setTestInput(e.target?.value)}
defaultValue={elData.label}
/>
// new code
<input
onChange={(e) => setTestInput(e.target?.value)}
value={testInput || ''}
/>
Can anyone explain the difference?

Fetch sum of column

I am using React-Admin to create little app for evidence of tools and who borrowed them.
I have table of tools with id, code, name, state atd.
using List to render it.
<List {...props} filters={<ToolFilter />}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="code" label="Kód" />
<TextField source="name" />
<TextField source="state" />
<NumberField source="free" />
<DateField source="add_time" />
<EditButton />
</Datagrid>
</List>
Than I have table B_tools, that holds data of borrowed tools.
it has id, userId, toolId, durationOfBorrow
What I want to do is to add column to the tools list, that SUMs durationOfBorrow from B_tool table for every tool and render it to list of tools.
for example if I have B_tool table:
id userId toolId durationOfBorrow
1 1 1 7
2 1 1 7
3 2 2 2
4 1 2 2
I need list of tools to look like this:
id code name state durationOfBorrow
1 123 Drill 1 14 (7+7)
2 456 Wrench 1 4 (2+2)
I tried use Querying The API With fetch from React-Admin documentation.
I have prepared route app.get('/api/tools/borrowcount:id', toolController.borrowCount); that shoud return sum of column:
borrowCount(req, res) {
//console.log(req);
const filtry = JSON.parse(req.query.filter);
console.log(filtry);
const options = {
attributes: [
[sequelize.fn('sum', sequelize.col('time')), 'total_time'],
],
raw: true,
where: ({
}),
order:
[]
,
};
if (typeof filtry.id !== "undefined") {
options.where.id = filtry.id;
}
return B_tool
.findAll(options)
//console.log(user);
//res.status(201).send(test);
.then(b_tool => {
//console.log(user.rows);
res.status(200).send(b_tool);
})
.catch(error => res.status(400).send(error));
},
But dont know how to implement it to show it in the tools list.
I got it.
I created element BorrowCount that useQuery api/toolsborrow/:id and returns total_time.
import React from 'react';
import { useQuery, Loading, Error } from 'react-admin';
const BorrowCount = ({ record }) => {
const { data, loading, error } = useQuery({
type: 'getOne',
resource: 'toolsborrow',
payload: { id: record.id }
});
if (loading) return <Loading />;
if (error) return <Error />;
if (!data) return null;
console.log(data);
return (
data[0].total_time
)
};
export default BorrowCount;
In API I call new controller:
app.get('/api/toolsborrow/:id', toolController.borrowCount);
And controller returns SUM of time column where ToolId is id of tool row:
borrowCount(req, res) {
const options = {
attributes: [
[Sequelize.fn('sum', Sequelize.col('time')), 'total_time'],
],
raw: true,
where: ({
ToolId: req.params.id
}),
order:
[]
,
};
return B_tool
.findAll(options)
.then(b_tool => {
res.status(200).send(b_tool);
})
.catch(error => res.status(400).send(error));
},
Finally, I just use this new element BorrowCount in List of tools:
<List {...props} filters={<ToolFilter />}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="code" label="Kód" />
<TextField source="name" />
<TextField source="state" />
<BorrowCount label="Total_sum"/>
<NumberField source="free" />
<DateField source="add_time" />
<EditButton />
</Datagrid>
</List>
If someone has more elegant solution, please leave it here.

Editing a Material UI row within React + Redux

I have a material UI table in a react project, and I want a user to click on the pencil/Edit Icon to edit the table row.
This is a loaded question, but would this require an additional piece of Material UI? what would the logic look like to make this happen?? I have most of the code set up on the backend, but don't know how to write this code in the component???
Thanks for looking!
//STYLE VARIABLE BOR MATERIAL BUTTON
const style = {
margin: 12
//
};
const mapStateToProps = (state) => ({
user: state.user,
reduxState: state.getExpense
});
class ExpenseTable extends Component {
constructor(props) {
super(props);
this.state = {
getExpense: []
};
}
isSelected = (index) => {
return this.state.selected.indexOf(index) !== -1;
};
handleRowSelection = (selectedRows) => {
this.setState({
selected: selectedRows,
});
};
//on page load, DISPATCH GET_EXPENSE is
//SENT TO expenseSaga which then
//goes to getExpenseReducer and appends EXPENSE_DATA to the
//DOM
componentDidMount() {
const { id } = this.props.match.params;
this.props.dispatch({ type: USER_ACTIONS.FETCH_USER });
this.props.dispatch({ type: 'GET_EXPENSE' });
}
componentDidUpdate() {
if (!this.props.user.isLoading && this.props.user.userName ===
null) {
this.props.history.push('home');
}
}
logout = () => {
this.props.dispatch(triggerLogout());
// this.props.history.push('home');
};
//SETS STATE FOR ALL INPUTS
handleChange = (name) => {
return (event) => {
this.setState({
[name]: event.target.value
});
};
};
//SUBMIT BUTTON- TRIGGERS DISPATCH TO EXPENSE SAGA TO ADD DATA
handleClick = () => {
console.log('add expense', this.state);
this.props.dispatch({
type: 'ADD_EXPENSE',
payload: this.state
});
};
//TRASH ICON-TRIGGERS DISPATCH TO EXPENSE SAGA DELETE
handleClickRemove = (id) => {
console.log('delete expense', this.state);
this.props.dispatch({
type: 'DELETE_EXPENSE',
payload: id
});
};
handleClickEdit = (row) => {
console.log('edit expense', this.state)
this.props.dispatch({
type: 'EDIT_EXPENSE',
payload: row
})
}
render() {
console.log('HEY-oooo expense render', this.state);
let content = null;
if (this.props.user.userName) {
//MAP OVER REDUX STATE.
const tableRows = this.props.reduxState.map((row) => {
//.MAP SEPARATES DATA INTO INDIVIDUAL ITEMS.
const { id, item_description, purchase_date,
item_price, item_link } = row;
return (
<TableRow selectable={false}>
{/* TABLE ROWS */}
<TableRowColumn>{item_description}
</TableRowColumn>
<TableRowColumn>{purchase_date}
</TableRowColumn>
<TableRowColumn>${item_price}</TableRowColumn>
<TableRowColumn><a href={item_link}>{item_link}
</a></TableRowColumn>
<TableRowColumn><EditIcon class="grow:hover"
onClick={() => {this.handleClickEdit(row)}} />
</TableRowColumn>
<TableRowColumn><TrashIcon onClick={() =>
{this.handleClickRemove(id);
}}/>
</TableRowColumn>
</TableRow>
// END TABLE ROWS
);
});
<div>
{/* FORM FOR ADDING EXPENSES(DATA) */}
<form id="expenseForm">
<h3>
Add a new <br />
expense
</h3>
<input
type="text"
id="fname"
name="fname"
placeholder="Item description"
onChange=
{this.handleChange('item_description')}
/>
<br />
<br />
<input
type="text"
id="lname"
name="lname"
placeholder="Item price"
onChange={this.handleChange('item_price')}
/>
<br />
<input
type="text"
id="lname"
name="lname"
placeholder="Item link"
onChange={this.handleChange('item_link')}
/>
<br />
{/* END FORM */}
<RaisedButton
id="expSubmit"
label="Submit Expense"
primary={true}
style={style}
onClick={this.handleClick}
/>
{/* TABLE TOTAL KEEPS CURRENT TOTAL OF PRICE COLOUMN */}
<h1>Total:</h1>
<br />
<h3>$748.93</h3>
</form>
{/* TABLE HEADERS */}
<Table>
<TableHeader>
<TableRow>
<TableHeaderColumn>Item
description</TableHeaderColumn>
<TableHeaderColumn>Purchase
Date</TableHeaderColumn>
<TableHeaderColumn>Item
Price</TableHeaderColumn>
<TableHeaderColumn>Item
Link</TableHeaderColumn>
<TableHeaderColumn>Edit
entry</TableHeaderColumn>
<TableHeaderColumn>Delete
entry</TableHeaderColumn>
</TableRow>
</TableHeader>
<TableBody>
{tableRows}
</TableBody>
</Table>
</div>
);
}
return (
<div>
<Nav />
{content}
</div>
);
}
}
// this allows us to use <App /> in index.js
export default connect(mapStateToProps)(ExpenseTable);
The default material-ui table doesn't have the feature of in-line editing. You will have to write your own wrapper to achieve this which can be time consuming. To overcome the time issue we can use some good existing libraries.
Here's one powerful library from DevExtreme for the same which i used, but go through their licensing before you take the final decision to use.
https://devexpress.github.io/devextreme-reactive/react/grid/demos/featured/controlled-mode/

React Router 4, How do I get a valid match url with route render or with withRouter?

The match object I receive using either the render property on the Route component or using an HOC from withRouter always produces a match object that is wrong. The location object is correct. The match url property is always '/'
Here is a code pen that shows the problem React Router 4 match woes
const { render } = ReactDOM
const {
HashRouter,
Route,
Link
} = ReactRouterDOM
const App = () => (
<HashRouter>
<div>
<AddressBar/>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/topics">Topics</Link></li>
</ul>
<hr/>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/topics" component={Topics}/>
</div>
</HashRouter>
)
const Home = () => (
<div>
<h2>Home</h2>
</div>
)
const About = ({ match }) => (
<div>
<h2>About</h2>
<h3>Match: {match.url}</h3>
</div>
)
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<ul>
<li><Link to={`${match.url}/rendering`}>Rendering with React</Link></li>
<li><Link to={`${match.url}/components`}>Components</Link></li>
<li><Link to={`${match.url}/props-v-state`}>Props v. State</Link></li>
</ul>
<Route path={`${match.url}/:topicId`} component={Topic}/>
<Route exact path={match.url} render={() => (
<h3>Please select a topic.</h3>
)}/>
</div>
)
const Topic = ({ match }) => (
<div>
<h3>{match.params.topicId}</h3>
<h3>Match: {match.url}</h3>
</div>
)
const AddressBar = () => (
<Route render={({ location: { pathname }, match: {url}, goBack, goForward }) => (
<div className="address-bar">
<div>
<button
className="ab-button"
onClick={goBack}
>◀︎</button>
</div>
<div>
<button
className="ab-button"
onClick={goForward}
>▶</button>
</div>
<div className="url">URL: {pathname} Match: {url}</div>
</div>
)}/>
)
render(<App/>, document.getElementById('root'))
Click on the About link (Topics has the same issue): notice the AddressBar, which uses Route render, at the top always shows
> Match: /
while at the bottom of the screen the Component displays the correct match.url
Match: /about
What am I doing wrong? How do I get a valid match object into the AddressBar?
Here is the ugly hack I used for my specific route where I need to know the specific 'league' parameter.
let keys = []
let path = '/*/:league/:id';
if (this.path.split('/').length === 3)
path = '/*/:league';
let re = pathToRegexp(path, keys);
const result = re.exec(this.path)
if (result && result[2])
this.store.setLeague(result[2]);
If I understand your question correctly you're looking to be able to use React Router's matching tools to pull paramaters out of a URL. This worked for me
import { matchPath } from 'react-router'
const match = matchPath('/users/123', {
path: '/users/:id',
exact: true,
strict: false
})
Source: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/matchPath.md
I guess whenever you need to use match, you also need to combine it with pathname
example
var match = this.props.match;
<Link className = 'xxx' to = {{pathname:match.url + '/target'}}