I'd like to add pagination to the latest posts gutenberg block. This block fetches posts through withSelect() and getEntityRecords(), similar to the block editor handbook.
The REST API returns two handy header fields for use in pagination:
"X-WP-Total" and "X-WP-TotalPages"
https://developer.wordpress.org/rest-api/using-the-rest-api/pagination/
Does anyone know if its possible to access these header fields in a dynamic block which fetches posts through withSelect() and getEntityRecords() and set state for the total number of pages and the current page?
Here's the simplyfied version of block.js:
( function( blocks, element, data ) {
var registerBlockType = blocks.registerBlockType,
withSelect = data.withSelect;
registerBlockType( 'mdwpb/latest-posts', {
title: 'latest posts',
icon: 'megaphone',
category: 'widgets',
attributes: {},
edit: withSelect( function( select ) {
return {
// here's where the magic happens, I think..
posts: select( 'core' ).getEntityRecords( 'postType', 'post', {per_page: 1, page: 1})
numberOfPages: ?,
};
} )( function( props ) {
if ( ! props.posts ) {
return "Loading...";
}
if ( props.posts.length === 0 ) {
return "No posts";
}
var className = props.className;
var post = props.posts[ 0 ];
return (
<div>
{ props.posts.map( ( post ) => (
<h3 className={props.className}>
{post.title.rendered}
</h3>
))}
</div>
);
} ),
} );
}(
window.wp.blocks,
window.wp.element,
window.wp.data,
) );
I tried adding the numberOfPages to the withSelect function through an apiFetch:
numberOfPages: wp.apiFetch({
path: wp.url.addQueryArgs( 'wp/v2/posts', {per_page: 1, page: 1} ),
parse: false,
}).then( response => { return response.headers.get('X-WP-TotalPages'); } ),
This kindof works, but I get a promise when I use console.log of the numberOfPages prop in the function(props). So I feel adding a apiFetch to the withSelect is not the way to go, or is it?
I haven't found a solution to properly get the number of pages, but what I did as a workaround is to run an additional getEntityRecords query for once with the same parameters as the main one, only switching the order the other way around (setting orderby parameter to asc or desc) and limiting it to only one item (per_page set to 1). From this I get the last item in my target set. Once I have that, I can compare the ID with all the ID's of the entities returned by the main pagination query. As soon as I find the ID of the last item in the set returned by the main query, I know this was the last page, so I can disable the "load next page" button or so.
It is not a full value solution, as it lacks the ability to tell in advance how many pages there will be, but still better than leaving the user clicking on "load more" to no avail until they give up.
Related
I've have started seeing the following notice in the web developer console when editing posts in Gutenberg:
wp.blockEditor.RichText multiline prop is deprecated since version 6.1 and will be removed in version 6.3. Please use nested blocks (InnerBlocks) instead.
I am unsure how I would go about converting my custom static Gutenberg block that currently uses <RichText> with the multiline property into a <div> with <InnerBlocks /> that still honor the original functionality that I built. This is a simplified example of what the edit() function currently looks like for the block:
edit: ( props ) => {
const blockProps = useBlockProps( { className: 'custom-cta p-0 mb-0' } );
const { attributes: { blurb }, setAttributes, className, isSelected } = props;
return (
<div { ...blockProps }>
<RichText
tagName="div"
multiline="p"
className="custom-cta__blurb w-100"
translate-name="blurb"
onChange={ value => setAttributes( { blurb: value } ) }
placeholder={ __( 'Subtext goes here (optional)', 'hello-tools' ) }
value={ blurb }
allowedFormats={ [ 'core/bold', 'core/italic' ] }
focusOnInsert={ false }
/>
</div>
);
}
This might come fairly close to what you are looking for. Try it - you might have to add some more attributes/settings and possibly CSS to get the exact result you are looking for.
This will not be able to replace your current block - so what I mean is, it is not backwards compatible, since it is a totally new block.
/**
* #see ./edit.js
*/
edit: () => {
// define which blocks to show when the block gets inserted
const TEMPLATE = [['core/paragraph']];
// define what blocks are allowed to be used
let allowedBlocks = ['core/paragraph'];
const blockProps = useBlockProps({className: 'my-custom-class'});
return (
<div {...blockProps}>
<div className={'custom-cta__blurb w-100'}>
<InnerBlocks
allowedBlocks={allowedBlocks}
template={TEMPLATE}
// false will let the user add and move the blocks
templateLock={false}
/>
</div>
</div>
)
}
```
I cant contribute to the discussion under the solution above, but I believe Frizzant is mistaken. Wordpress has included a solution in their list item block on GitHub, but I do not understand how to implement it.
Struggle to make virtualized list scrolled to a particular row after content refresh. Scenario: there are few rows, user scrolls to somewhere in the list, and then triggers an event (e.g. presses a button in the app) that modifies the content of the list items. Ideally the user must see the same row at the top of the scroll view that was before the event occurred.
Here is the snippet from my code where I try to make it work with no success.
class MyList extends React.Component {
state = {
newScrollToIndex: undefined
}
// function triggered from outsided events
onCellContentChanged() {
const index = this.listRowIndex
// Don't know really which one to call exactlty
// Just called everything
this.cellMeasurerCache.clearAll()
this.listView.recomputeRowHeights()
this.listView.measureAllRows()
/*
* Tried this did not work at all.
* const off = this.listView.getOffsetForRow({ index })
* this.listView.scrollToPosition(off)
*/
// This two seem to work equivalently with 50% chance to work correctly.
// this.listView.scrollToRow(index)
this.setState({ newScrollToIndex: index })
}
renderInfiniteList({ height, width }) {
return (
<InfiniteLoader
isRowLoaded={this.isRowLoaded}
loadMoreRows={this.loadMoreRows}
rowCount={this.rowCount}
>
{({ onRowsRendered, registerChild }) => {
return (
<List
style={{ outline: 'none' }}
noRowsRenderer={() => (
<NoRows
loading={
this.state.loadingMoreRows || this.state.loadingFields
}
/>
)}
height={height}
width={width}
overscanRowCount={2}
rowCount={this.listViewRowCount}
rowHeight={this.cellMeasurerCache.rowHeight}
deferredMeasurementCache={this.cellMeasurerCache}
rowRenderer={this.rowRenderer}
scrollToIndex={this.state.newScrollToIndex}
onRowsRendered={(o) => {
onRowsRendered(o)
this.listRowIndex = o.startIndex
}}
ref={(listView) => {
registerChild(listView)
this.listView = listView
}}
/>
)
}}
</InfiniteLoader>
)
}
}
Any clarification on what is the proper way to make of this scenario to work is appreciated.
I'm currently using Material-table . It displays data normally however, Pagination and Row per Page dropdown is not working. Nothing happens upon clicking, next button and selected number of rows.
See below codes:
import MaterialTable from 'material-table'
const tableIcons = {
/*table icons*/
}
function Test(){
const [data, setData] = useState([]);
const getDatas = async() => {
await axios.get('/API')
.then(response => {
setData(response.data)
}
}
const columns = [
{.....} //columns
]
return(
<div>
<MaterialTable
icons = {tableIcons}
columns = {columns}
data = {data}
title = 'List of data'
actions = {[{
//add button properties
}]}
>
</MaterialTable>
</div>
)
}
export default Test;
I'm getting the following error on console upon onload and clicking pagination buttons.
On load:
On click of next button
Please help me with this. Thank you in advance.
First of all, keep in mind that the original project was discontinued, and the new direction can be found in this repository (it's a fork of the original). There will be a lot of refactorings and breaking changes, so you might want to check them out first.
Now, on your question,
since you are working with remote data you could check out the official example on how to handle this kind of data.
If your requirements don't allow you to do this, you will need to do all the handling by yourself. That means you should provide your own implementation of the Pagination component, in which you define your own behavior of onChangePage and other callbacks.
The customisation will look something like:
Pagination: (properties: any) => {
return (
<TablePagination
{...properties}
count={currentPage.total}
onChangePage={(event: any, page: number) => {
onChangePage(page);
}}
page={currentPage.startIndex / pageSize}
/>
);
}
where total, startIndex etc. will be provided by the API you consume, along with the actual data that you show in the table.
These components overrides should be provided under the components property of the material table.
I'm building a site that uses Vue for to power the majority of the UI. The main component is a list of videos that is updated whenever a certain URL pattern is matched.
The main (video-list) component looks largely like this:
let VideoList = Vue.component( 'video-list', {
data: () => ({ singlePost: '' }),
props: ['posts', 'categorySlug'],
template: `
<div>
<transition-group tag="ul">
<li v-for="(post, index) in filterPostsByCategory( posts )">
<div #click.prevent="showPost( post )">
<img :src="post.video_cover" />
/* ... */
</div>
</li>
</transition-group>
</div>`,
methods: {
orderPostsInCategory: function ( inputArray, currentCategory ) {
let outputArray = [];
for (let i = 0; i < inputArray.length; i++) {
let currentCategoryObj = inputArray[i].video_categories.find( (category) => {
return category.slug === currentCategory;
});
let positionInCategory = currentCategoryObj.category_post_order;
outputArray[positionInCategory] = inputArray[i];
}
return outputArray;
},
filterPostsByCategory: function ( posts ) {
let categorySlug = this.categorySlug,
filteredPosts = posts.filter( (post) => {
return post.video_categories.some( (category) => {
return category.slug === categorySlug;
})
});
return this.orderPostsInCategory( filteredPosts, categorySlug );
}
}
});
The filterPostsByCategory() method does its job switching between the various possible categories, and instantly updating the list, according to the routes below:
let router = new VueRouter({
mode: 'history',
linkActiveClass: 'active',
routes: [
{ path: '/', component: VideoList, props: {categorySlug: 'home-page'} },
{ path: '/category/:categorySlug', component: VideoList, props: true }
]
});
The difficulty I'm having is transitioning the list in the way that I'd like. Ideally, when new category is selected all currently visible list items would fade out and the new list items would then fade in. I've looked at the vue transitions documentation, but haven't been able to get the effect I'm after.
The issue is that some items have more than one category, and when switching between these categories, those items are never affected by whatever transition I try to apply (I assume because Vue is just trying to be efficient and update as few nodes as possible). It's also possible that two or more categories contain the exact same list items, and in these instances enter and leave methods don't seem to fire at all.
So the question is, what would be a good way to ensure that I can target all current items (regardless of whether they're still be visible after the route change) whenever the route patterns above are matched?
Have you noticed the special key attribute in the documentation?
Vue.js is really focused on performance, because of that, when you modify lists used with v-for, vue tries to update as few DOM nodes as possible. Sometimes it only updates text content of the nodes instead of removing the whole node and then append a newly created one. Using :key you tell vue that this node is specifically related to the given key/id, and you force vue to completely update the DOM when the list/array is modified and as a result the key is changed. In your case is appropriate to bind the key attribute to some info related to the post and the category filter itself, so that whenever the list is modified or the category is changed the whole list may be rerendered and thus apply the animation on all items:
<li v-for="(post, index) in filterPostsByCategory( posts )" :key="post.id + categorySlug">
<div #click.prevent="showPost( post )">
<img :src="post.video_cover" />
/* ... */
</div>
</li>
i'm struggeling with one challenge to do in my custom theme for Wordpress. I want to have content specific controls in my Theme Customizer. I know there is option "active_callback", but this is not sufficient for my purpose and i read 2 documentation articles about customizer and this https://make.wordpress.org/core/2014/07/08/customizer-improvements-in-4-0/ article, but still have no clue, here is what i want to achieve:
For example, i want to have "show sidebar" checkbox, but this checkbox should be more contextual specifix. For example, when i will be on homepage, there will be just one checkbox as "Show sidebar default" but when i will go into some post, i want there 3 checkboxes:
"Show sidebar default" - id="show_sidebar"
"Show sidebar in Post archive page" - id="show_sidebar_archive_{post_type}"
"Show sidebar for this post" - id="show_sidebar_singular_{post_id}"
So when i want to have this kind of specific IDs for control, just active_callback is not enought, becauce it can just show/hide controls, i can't create new when URL in iframe changes.
There could be 2 sollutions:
1. Better - when i could somehow create/remove controls by context, it would be best solution. If it's somehow possible with customizer API, give me som hint please
2. Not good, but sufficient - is at least possible somehow reload whole /wp-admin/customize.php?url= with new clicked url? this could be enought for a while
thx for any advices!
Ok, i figured out that second solution, here is code. It's enought for me for now.
'use strict';
(function ($) {
/**
* This is important fix for Theme Customizer
* It will change whole customizer url, because we need to load
* all controls ahan for new url, because of hierarchical options
*/
if ( /\/customize\.php$/.test( window.location.pathname ) ) {
wp.customize.bind( 'preview-ready', function() {
var body = $( 'body' );
body.off( 'click.preview' );
body.on( 'click.preview', 'a[href]', function( event ) {
var link, isInternalJumpLink;
link = $( this );
isInternalJumpLink = ( '#' === link.attr( 'href' ).substr( 0, 1 ) );
event.preventDefault();
if ( isInternalJumpLink && '#' !== link.attr( 'href' ) ) {
$( link.attr( 'href' ) ).each( function() {
this.scrollIntoView();
} );
}
/*
* Note the shift key is checked so shift+click on widgets or
* nav menu items can just result on focusing on the corresponding
* control instead of also navigating to the URL linked to.
*/
if ( event.shiftKey || isInternalJumpLink ) {
return;
}
//self.send( 'scroll', 0 );
//self.send( 'url', link.prop( 'href' ) );
var newUrl = link.prop( 'href' );
var currentUrl = window.location.href;
var customizeUrl = currentUrl.substring(0, currentUrl.indexOf("?url=") + 5) + newUrl;
// Reload whole customizer for new url
window.parent.location.replace(customizeUrl);
});
} );
}
})(jQuery);
//# sourceMappingURL=customizer.js.map