I'm implementing pagination functionality by semantic-ui-react.
I can implement pagination component itself, but can't implement onPageChange to set activePage and control number of pages displayed.
I use react for client side functionality.
Also I use semantic-ui-react for css framework.
All contents of array is listed on single page now.
But I want to implement pagination and limit to display 5 contents on single page.
I somehow understand I need to use onPageChange, but don't know how to implement to achieve that goal.
import React from 'react';
import {Link} from 'react-router-dom';
import {Grid, Segment, Container, Header, Pagination} from 'semantic-ui-react';
import axios from 'axios';
import {Article} from '../articleData';
interface ArticleState {
articles: Article[];
}
class List extends React.Component<{}, ArticleState> {
constructor(props: {}) {
super(props);
this.state = {
articles: [],
};
this.serverRequest = this.serverRequest.bind(this);
this.btnClick = this.btnClick.bind(this);
}
serverRequest() {
axios
.get('/api/articles')
.then(response => {
this.setState({articles: response.data});
})
.catch(response => console.log('ERROR!! occurred in Backend.'));
}
btnClick(event: React.MouseEvent<HTMLAnchorElement>, data: object) {
}
componentDidMount() {
this.setState({articles: []});
this.serverRequest();
}
render() {
return (
<Container style={{marginTop: '7em'}} text>
<Grid columns={1} divided="vertically">
<Grid.Row>
{(this.state.articles || []).map(function(articleData, i) {
return (
<Grid.Column>
<Segment>
<Header as="h1">{articleData.title}</Header>
<p>{articleData.content}</p>
<Link to={`/detail/${articleData.id}`}>
continue reading
</Link>
</Segment>
</Grid.Column>
);
})}
</Grid.Row>
</Grid>
<Pagination
defaultActivePage={5}
totalPages={Math.floor(this.state.articles.length / 2) + 1}
//onPageChange={this.btnClick}
/>
</Container>
);
}
}
export default List;
I expect the pagination functionality to limit number of displayed content to 5 on single page.
But actually I don't know how to implement this functionality.
I resolved this issue.
Here is the code:
import React from 'react';
import {Link} from 'react-router-dom';
import {
Grid,
Segment,
Container,
Header,
Pagination,
PaginationProps,
Icon,
} from 'semantic-ui-react';
import axios from 'axios';
import {Article} from '../articleData';
interface ArticleState {
articles: Article[];
articleDatas: Article[];
begin: number;
end: number;
activePage: number;
}
class List extends React.Component<{}, ArticleState> {
constructor(props: {}) {
super(props);
this.state = {
articles: [],
articleDatas: [],
begin: 0,
end: 5,
activePage: 1,
};
this.serverRequest = this.serverRequest.bind(this);
this.btnClick = this.btnClick.bind(this);
}
async serverRequest() {
const res = await axios.get('/api/articles');
this.setState({articles: res.data});
}
async btnClick(
event: React.MouseEvent<HTMLAnchorElement>,
data: PaginationProps
) {
await this.setState({activePage: data.activePage as number});
await this.setState({begin: this.state.activePage * 5 - 5});
await this.setState({end: this.state.activePage * 5});
this.setState({
articleDatas: this.state.articles.slice(this.state.begin, this.state.end),
});
}
async componentDidMount() {
this.setState({articles: []});
await this.serverRequest();
this.setState({
articleDatas: this.state.articles.slice(this.state.begin, this.state.end),
});
}
render() {
return (
<Container style={{marginTop: '3em'}} text>
<Grid columns={1} divided="vertically">
<Grid.Row>
{(this.state.articleDatas || []).map(function(articleData, i) {
return (
<Grid.Column>
<Segment>
<Header as="h1">{articleData.title}</Header>
<p>{articleData.content}</p>
<Link to={`/detail/${articleData.id}`}>
continue reading
</Link>
</Segment>
</Grid.Column>
);
})}
</Grid.Row>
</Grid>
<Pagination
defaultActivePage={1}
totalPages={Math.ceil(this.state.articles.length / 5)}
onPageChange={this.btnClick}
/>
</Container>
);
}
}
export default List;
From the semantic-ui docs, onPageChange is a function with two arguments:
event - React's original SyntheticEvent
data - An object containing all props
Looking inside the data object, there is a key named activePage which represents the new page number when the pagination is changed.
Here's a very basic example that demonstrates this, by logging the newly selected page number to the console.
<Pagination
defaultActivePage={5}
totalPages={10}
onPageChange={(event, data) => console.log(data.activePage)}
/>
However, I think you're expectation of how Pagination in Semantic UI works may not be correct. The Pagination component purely renders a clickable list of page numbers, it does not handle the display of results or limits the number of results displayed.
This is something that is generally handled in the API. A common approach is to have a ?page=x query string parameter (or sometimes ?offset=) in the URL. You could then use the activePage value from the Semantic Pagination component to indicate to the server which page of results to return.
Related
I made a request to google books API, trying to retrieve the volumes (a.k.a books) from my bookshelf. But the response I get contains only 10 items whereas on the shelf I have 17 items.
Could this be that my code is faulty, or maybe a restriction from google ?
see my code for making the call and for mapping it.
My api.js
import React, { Component } from "react"
import request from 'superagent';
import BookList from './BookList';
class Book extends Component{
constructor(props){
super(props);
this.state = {
books: [],
}
}
componentDidMount(){
request.get("https://www.googleapis.com/books/v1/users/101###############/bookshelves/4/volumes?key=AIzaSyDNMnPGw3################-PecbhU")
.query(null)
.then((data) =>{
console.log(data);
this.setState({books: [...data.body.items]})
})
}
render(){
return(
<div>
<BookList books={this.state.books}/>
</div>
);
}
}
export default Book
And here i map
import React, { Component } from "react"
import BookCard from './bookcard';
const BookList = (props) => {
if(!props){
return null;
}
return(
<div>
{
props && props.books?.map((book, i) => {
return <div class="grid-container">
<BookCard
key={i}
image={book.volumeInfo.imageLinks.thumbnail}
title={book.volumeInfo.title}
author={book.volumeInfo.authors}
url={book.volumeInfo.canonicalVolumeLink}
rating={book.volumeInfo.averageRating}
/>
</div>
})
}
</div>
)
}
export default BookList
As the docs say:
Pagination
You can paginate the volumes list by specifying two values in the parameters for the request:
startIndex - The position in the collection at which to start. The index of the first item is 0.
maxResults - The maximum number of results to return. The default is 10, and the maximum allowable value is 40.
So you can add maxResults=40 to your query to get more results at once.
I just started teaching myself ReactJS a few weeks ago and I'm stuck trying to figure out why my API gets consistently hit in an infinite loop after selecting a value from a dropdown. I have a search component called StateSearch.js that is being rendered in the StatePolicyPage.js component.
In StatePolicyPage.js I call <StateSearch parentCallback={this.callbackFunction} /> so that I can get the value the user picked from the dropdown and set the state. In StateSearch.js I'm passing the selected value using props.parentCallback(response.data)
The problem is that an infinite loop occurs for some reason and my Rails API keeps getting called over and over instead of just returning the data one time.
(StateSearch.js) search component
import React, { useState } from 'react'
import Select from 'react-select'
import makeAnimated from 'react-select/animated';
import axios from 'axios';
import statesJSON from '../../helpers/states';
// uses 'react-select'
export default function StateSearch(props) {
const [americanState, setAmericanState] = useState();
// if a state was selected from the dropdown
if (americanState) {
axios.get("http://localhost:3001/get_stuff", {
params: {
state: americanState.value
}
}).then(response => {
// the response back from the Rails server
if (response.status === 200) {
props.parentCallback(response.data); // send data back up to parent
}
}).catch(error => {
console.log("Error fetching the state ", americanState.value, error);
})
event.preventDefault();
}
// the dropdown select box for states.
return (
<div>
<Select
options={statesJSON}
placeholder="Select a State"
onChange={setAmericanState}
noOptionsMessage={() => 'Uh-oh nothing matches your search'}
className=""
components={makeAnimated()}
isSearchable
isClearable={true}
/>
</div>
)
}
(StatePolicyPage.js) the component that the search results should be passed to
import React, { Component } from 'react'
import Navigation from './Navigation';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import StateSearch from './search/StateSearch';
export default class StatePolicyPage extends Component {
constructor(props) {
super(props);
this.state = {
id: '',
stateName: '',
updatedAt: '',
createdAt: ''
}
}
callbackFunction = (childData) => {
console.log(childData);
this.setState({
id: childData.id,
stateName: childData.state_name,
updatedAt: childData.updated_at,
createdAt: childData.created_at
})
}
render() {
return (
<div>
<Navigation
isLoggedIn={this.props.loggedInStatus}
user={this.props.user}
handleLogoutClick={this.props.handleLogoutClick}
handleLogout={this.props.handleLogout}
/>
<Container>
{/* get the dropdown value from the StateSearch back */}
<StateSearch parentCallback={this.callbackFunction} />
<div>
<Row>
{ this.state.id }
</Row>
</div>
</Container>
</div>
)
}
}
Always use useEffect() hook for asynchronous tasks.
useEffect(() => {
// if a state was selected from the dropdown
if (americanState) {
axios.get("http://localhost:3001/get_stuff", {
params: {
state: americanState.value
}
}).then(response => {
// the response back from the Rails server
if (response.status === 200) {
props.parentCallback(response.data); // send data back up to parent
}
}).catch(error => {
console.log("Error fetching the state ", americanState.value, error);
})
}
}, [americanState]);
This line looks to me like it could cause an infinite loop:
components={makeAnimated()}
I'm not entirely sure what this function is doing, but when passing functions to another component you can't directly invoke them.
Try replacing the above line with this:
components={makeAnimated}
or with this:
components={() => makeAnimated()}
I've created a form in react and after some research i think that if you don't want to use an external library to manage the form, the context could be the best choice, expecially in my case where i've many nested component that compose it.
But, i'm not sure that putting a function inside my state is a good thing.
But let me give you some code:
configuration-context.js
import React from 'react'
export const ConfigurationContext = React.createContext();
ConfigurationPanel.jsx
import React, { Component } from 'react'
import { Header, Menu, Grid } from 'semantic-ui-react'
import ConfigurationSection from './ConfigurationSection.jsx'
import {ConfigurationContext} from './configuration-context.js'
class ConfigurationPanel extends Component {
constructor(props) {
super(props)
this.state = {
activeItem: '',
configuration: {
/* the configuration values */
banana: (data) => /* set the configuration values with the passed data */
}
}
}
handleItemClick = (e, { name }) => this.setState({ activeItem: name })
render() {
return (
<ConfigurationContext.Provider value={this.state.configuration}>
<Grid.Row centered style={{marginTop:'10vh'}}>
<Grid.Column width={15} >
<div className='configuration-panel'>
/* SOME BUGGED CODE */
<div className='configuration-section-group'>
{this.props.data.map((section, i) => <ConfigurationSection key={i} {...section} />)}
</div>
</div>
</Grid.Column>
</Grid.Row>
</ConfigurationContext.Provider>
)
}
}
ConfigurationItem.jsx
import React, { Component } from 'react'
import { Input, Dropdown, Radio } from 'semantic-ui-react'
import {ConfigurationContext} from './configuration-context.js'
class ConfigurationItem extends Component {
static contextType = ConfigurationContext
constructor(props) {
super(props)
}
handleChange = (e, data) => this.context.banana(data)
itemFromType = (item) =>{
switch (item.type) {
case "toggle":
return <div className='device-configuration-toggle-container'>
<label>{item.label}</label>
<Radio name={item.name} toggle className='device-configuration-toggle'onChange={this.handleChange} />
</div>
/* MORE BUGGED CODE BUT NOT INTERESTING*/
}
}
render() {
return this.itemFromType(this.props.item)
}
}
So, at the end i've a ConfigurationContext that is just a declaration, everything is inside the parent state.
The thing that i don't like is putting the banana function inside the state (it will have more logic that just logging it)
What do you think about it?
Any suggestion is appreciated.
Thanks
banana is just a regular function and you do not have to put it in the state, just do:
class ConfigurationPanel extends Component {
banana = data => console.log(data)
...
render() {
return (
<ConfigurationContext.Provider value={{banana}}>
...
}
After that you can use this.context.banana(data) as normal.
I'm getting the error: You started loading 'Roboto_medium', but used it before it finished loading when using native base.
I've followed the instructions in the official page.
To create react native app I'm using create-react-native-app.
App.js
export default class App extends React.Component {
async componentWillMount() {
await Expo.Font.loadAsync({
'Roboto': require('native-base/Fonts/Roboto.ttf'),
'Roboto_medium': require('native-base/Fonts/Roboto_medium.ttf'),
'Ionicons': require('#expo/vector-icons/fonts/Ionicons.ttf'),
});
}
render() {
return (
<Container>
<StatusBar hidden={true} />
<Button>
<Text>
Button
</Text>
</Button>
<ListaItens />
</Container>
);
}
}
you need to wait till the fonts get loaded. You can do something like this
import React from "react";
import { StatusBar } from "react-native";
import { Container, Button, text, ListItem, Text } from "native-base";
import Expo from "expo";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = { loading: true };
}
async componentWillMount() {
await Expo.Font.loadAsync({
Roboto: require("native-base/Fonts/Roboto.ttf"),
Roboto_medium: require("native-base/Fonts/Roboto_medium.ttf"),
Ionicons: require("#expo/vector-icons/fonts/Ionicons.ttf"),
});
this.setState({ loading: false });
}
render() {
if (this.state.loading) {
return <Expo.AppLoading />;
}
return (
<Container>
<StatusBar hidden={true} />
<Button>
<Text>Button</Text>
</Button>
<ListItem />
</Container>
);
}
}
This new code for expo SDK 35 which was modified from #akhil xavier 's answer
First install expo-font
expo install 'expo-font'
here is the App.js
import React from "react";
import * as Font from "expo-font";
import { ActivityIndicator } from "react-native";
import { Root } from "native-base";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = { loading: true };
}
async componentWillMount() {
await Font.loadAsync({
Roboto: require("native-base/Fonts/Roboto.ttf"),
Roboto_medium: require("native-base/Fonts/Roboto_medium.ttf"),
Ionicons: require("#expo/vector-icons/build/vendor/react-native-vector-icons/Fonts/Ionicons.ttf"),
});
this.setState({ loading: false });
}
render() {
if (this.state.loading) {
return <ActivityIndicator />;
}
return (
<Root>
<RootPage /> // starter component (i.e. nav)
</Root>
);
}
}
The solution that works for me is below. The error I had was that I imported Font as (Fonts) but while calling it failed to notice the 's'. Fixed it by making sure the import name is similar to what you call the loadAsync to. You need to have 'expo-font' installed in your project for it to work
import React from "react";
import * as Font from "expo-font";
import { AppLoading } from "expo";
import MealsNavigator from "./navigation/MealsNavigator";
const fetchFonts = () => {
return Font.loadAsync({
"open-sans": require("./assets/fonts/OpenSans-Regular.ttf"),
"open-sans-bold": require("./assets/fonts/OpenSans-Bold.ttf")
});
};
export default function App() {
const [fontLoaded, setFontLoaded] = useState(false);
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => setFontLoaded(true)}
onError={err => console.log(err)}
/>
);
}
return <MealsNavigator />;
}
If anyone is having this issue with the 'MaterialIcons' font family, I had a similar problem and found following this solution worked:
https://javascriptrambling.blogspot.com.au/2018/03/expo-icon-fonts-with-react-native-and.html
You basically need to:
Install the fonts (using npm install)
Do a Font.loadAsync on the fonts in your componentWillMount() function.
Remember to mark the componentWillMount() function as async
The conditionally display either as 'loading' or with the view depending on the state of a 'loaded' flag.
For example:
import React from 'react';
import { View } from 'react-native';
import { Avatar } from 'react-native-elements';
import { AppLoading, Font } from 'expo';
import FontAwesome
from './node_modules/#expo/vector-icons/fonts/FontAwesome.ttf';
import MaterialIcons
from './node_modules/#expo/vector-icons/fonts/MaterialIcons.ttf';
export default class App extends React.Component {
state = {
fontLoaded: false
};
async componentWillMount() {
try {
await Font.loadAsync({
FontAwesome,
MaterialIcons
});
this.setState({ fontLoaded: true });
} catch (error) {
console.log('error loading icon fonts', error);
}
}
render() {
if (!this.state.fontLoaded) {
return <AppLoading />;
}
return (
<View>
<Text>My App</Text>
<Avatar
small
rounded
icon={{ name: 'add' }}
/>
</View>
);
}
}
This is the way which I use to load Fonts in my project. I feel this more abstracted as compared to all the other answers.
What you need to do is create a custom hook to Load fonts
You can do it something like this
Create a folder called hooks where your App.js is located. Then inside hooks folder create a file called useFonts.js
Inside useFonts.js write like this
import * as Font from 'expo-font';
export default useFonts = async () =>
await Font.loadAsync({
Roboto: require('native-base/Fonts/Roboto.ttf'),
Roboto_medium: require('native-base/Fonts/Roboto_medium.ttf'),
Ionicons: require('#expo/vector-icons/fonts/Ionicons.ttf'),
});
Now in your App.js write like this
import * as Font from 'expo-font';
import AppLoading from 'expo-app-loading';
import React, { useState } from 'react';
import useFonts from './hooks/useFonts';
export default function App() {
const [IsReady, SetIsReady] = useState(false);
// This function will start the fontLoading
const LoadFonts = async () => {
await useFonts();
};
// This is a check to ensure fonts get loaded
if (!IsReady) {
return (
<AppLoading
startAsync={LoadFonts}
onFinish={() => SetIsReady(true)}
onError={() => {}}
/>
);
}
// If fonts are successfully loaded then show the rest of your App
return (
<Container>
<StatusBar hidden={true} />
<Button>
<Text>Button</Text>
</Button>
<ListaItens />
</Container>
);
}
One reason why it has to load font is because you are using the Native Base Text component. If you import the React Native Text component instead you won't even have to load fonts and you won't see that error.
I am trying to get a list to refresh after a custom action was successfully executed.
i used the saga from the admin on rest tutorial
function * actionApproveSuccess () {
yield put(showNotification('Executed'))
yield put(push('/comments'))
// does not refresh, because the route does not change
// react-redux-router also has no refresh() method, like react-router has...
}
the other idea i had was to somehow trigger the refresh action of the list component, but i have no idea how to access that or how to hook that up to the ACTION_SUCCESS event.
There is no way to refresh a route via react router, and that's a known problem. Admin-on-rest's List component has its own refresh mechanism, but offers no API for it.
My advice would be to use a custom <List> component based on admin-on-rest's one. And if you find a way to expose the refresh action, feel free to open a PR on the aor repository!
#Danila Smirnov's answer above shows this message when I use it now:
Deprecation warning: The preferred way to refresh the List view is to connect your custom button with redux and dispatch the refreshView action.
Clicking the refresh button itself wasn't working either nowadays.
Here's the tweaked version that I got working in mine.
Edit: Modified it a bit more to make it reusable.
RefreshListActions.js
import React, { Component } from 'react'
import FlatButton from 'material-ui/FlatButton'
import { CardActions } from 'material-ui/Card'
import NavigationRefresh from 'material-ui/svg-icons/navigation/refresh'
import { connect } from 'react-redux'
import { REFRESH_VIEW } from 'admin-on-rest/src/actions/uiActions'
import { refreshView as refreshViewAction } from 'admin-on-rest/src/actions/uiActions'
class MyRefresh extends Component {
componentDidMount() {
const { refreshInterval, refreshView } = this.props
if (refreshInterval) {
this.interval = setInterval(() => {
refreshView()
}, refreshInterval)
}
}
componentWillUnmount() {
clearInterval(this.interval)
}
render() {
const { label, refreshView, icon } = this.props;
return (
<FlatButton
primary
label={label}
onClick={refreshView}
icon={icon}
/>
);
}
}
const RefreshButton = connect(null, { refreshView: refreshViewAction })(MyRefresh)
const RefreshListActions = ({ resource, filters, displayedFilters, filterValues, basePath, showFilter, refreshInterval }) => (
<CardActions>
{filters && React.cloneElement(filters, { resource, showFilter, displayedFilters, filterValues, context: 'button' }) }
<RefreshButton primary label="Refresh" refreshInterval={refreshInterval} icon={<NavigationRefresh />} />
</CardActions>
);
export default RefreshListActions
In my list that I want to refresh so often:
import RefreshListActions from './RefreshListActions'
export default (props) => (
<List {...props}
actions={<RefreshListActions refreshInterval="10000" />}
>
<Datagrid>
...
</Datagrid>
</List>
)
Definitely hacky, but a work-around could be:
push('/comments/1') //any path to change the current route
push('/comments') //the path to refresh, which is now a new route
using refreshView action via redux works well.
see example....
import { refreshView as refreshViewAction } from 'admin-on-rest';
import { connect } from 'react-redux';
class MyReactComponent extends Component {
//... etc etc standard react stuff...
doSomething() {
// etc etc do smt then trigger refreshView like below
this.props.refreshView();
}
render() {
return <div>etc etc your stuff</div>
}
}
export default connect(undefined, { refreshView: refreshViewAction })(
MyReactComponent
);
I've solve this task with small hack via Actions panel. I'm sure it is not correct solution, but in some situations it can help:
class RefreshButton extends FlatButton {
componentDidMount() {
if (this.props.refreshInterval) {
this.interval = setInterval(() => {
this.props.refresh(new Event('refresh'))
}, this.props.refreshInterval)
}
}
componentWillUnmount() {
clearInterval(this.interval)
}
}
const StreamActions = ({ resource, filters, displayedFilters, filterValues, basePath, showFilter, refresh }) => (
<CardActions>
{filters && React.cloneElement(filters, { resource, showFilter, displayedFilters, filterValues, context: 'button' }) }
<RefreshButton primary label="Refresh streams" onClick={refresh} refreshInterval={15000} refresh={refresh} icon={<NavigationRefresh />} />
</CardActions>
);
export default class StreamsListPage extends Component {
render() {
return (
<List
{...this.props}
perPage={20}
actions={<StreamActions />}
filter={{ active: true }}
title='Active Streams'>
<StreamsList />
</List>
)
}
}
The push is just a redirect for AOR which did not seem to work for me either. What guleryuz posted was on the right track for me.. Here's what I did building on his example:
// Import Statement
import { refreshView as refreshViewAction } from 'admin-on-rest';
class RemoveButton extends Component {
handleClick = () => {
const { refreshView, record, showNotification } = this.props;
fetch(`http://localhost:33333/api/v1/batch/stage/${record.id}`, { method: 'DELETE' })
.then(() => {
showNotification('Removed domain from current stage');
refreshView();
})
.catch((e) => {
console.error(e);
showNotification('Error: could not find domain');
});
}
render() {
return <FlatButton secondary label="Delete" icon={<DeleteIcon />}onClick={this.handleClick} />;
}
}
These bits are important as well:
RemoveButton.propTypes = {
record: PropTypes.object,
showNotification: PropTypes.func,
refreshView: PropTypes.func,
};
export default connect(null, {
showNotification: showNotificationAction,
refreshView: refreshViewAction,
})(RemoveButton);
So the way this works is it uses AOR's refreshViewAction as a prop function. This uses the underlying call to populate the data grid for me which is GET_LIST. This may not apply to your specific use case. Let me know if you have any questions.
Pim Schaaf's solution worked like a charm for me, Mine looks a bit different
yield put(push('/comments/-1')); // This refreshes the data
yield put(showNotification('')); // Hide error