Skip to content

Instantly share code, notes, and snippets.

@mercuryseries
Last active September 18, 2023 15:55
Show Gist options
  • Save mercuryseries/00081e3b8de000eb249ecef5472c2dab to your computer and use it in GitHub Desktop.
Save mercuryseries/00081e3b8de000eb249ecef5472c2dab to your computer and use it in GitHub Desktop.
infinite scroll
[data-controller="infinite-scroll"] [data-infinite-scroll-target="loader"] {
display: none;
}
.infinite-scroll--busy [data-infinite-scroll-target="loader"] {
display: block;
}
<div {{ stimulus_controller('infinite-scroll', {
endpointUrl: path(app.current_route, app.request.get('_route_params', [])),
pageCount: page_count,
rootMargin: '100px'
}, controllerClasses = {loading: 'infinite-scroll--busy'}) }}>
<div {{ stimulus_target('infinite-scroll', 'entries') }}>
{{ include('shared/_posts.html.twig') }}
</div>
<div {{ stimulus_target('infinite-scroll', 'trigger') }}></div>
<div {{ stimulus_target('infinite-scroll', 'loader') }}>
Loading...
</div>
</div>
<?php
namespace App\Controller;
use Knp\Component\Pager\PaginatorInterface;
use function Symfony\Component\String\u;
// ...
class HomeController extends AbstractController
{
public function index(Request $request, PostRepository $postRepository, PaginatorInterface $paginator): Response
{
$page = $request->query->getInt('page', 1);
/** @var \Knp\Bundle\PaginatorBundle\Pagination $pager */
$pager = $paginator->paginate(
$postRepository->createTimelineQueryBuilder($this->getUser()),
$page,
Post::NUM_ITEMS_PER_PAGE
);
if ($this->wantsJson($request)) {
return $this->json([
'entries' => $this->renderView('shared/posts.html.twig', [
'pager' => $pager,
'page_count' => $pageCount,
]),
]);
} else {
return $this->render('home.html.twig', [
'pager' => $pager,
'page_count' => $pageCount,
]);
}
}
// peut être déplacé au niveau d'un trait si tu l'utilises à plein d'endroits
private function wantsJson(Request $request)
{
$acceptable = $request->getAcceptableContentTypes();
return isset($acceptable[0]) && u($acceptable[0])->ignoreCase()->containsAny(['/json', '+json']);
}
}
import { Controller } from "@hotwired/stimulus";
export default class extends Controller<HTMLDivElement> {
page: number = 1;
intersectionObserver!: IntersectionObserver;
declare readonly entriesTarget: HTMLInputElement;
declare readonly triggerTarget: HTMLInputElement;
declare readonly loaderTarget: HTMLInputElement;
declare readonly endpointUrlValue: string;
declare readonly pageCountValue: number;
declare readonly rootMarginValue: string;
declare readonly thresholdValue: number;
declare readonly loadingClass: string;
static targets = ["entries", "trigger", "loader"];
static values = {
endpointUrl: String,
pageCount: Number,
rootMargin: { type: String, default: "0px" },
threshold: { type: Number, default: 0 },
};
static classes = ["loading"];
initialize(): void {
this.intersectionObserver = new IntersectionObserver(
(entries) =>
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.fetchEntries();
}
}),
{
rootMargin: this.rootMarginValue,
threshold: this.thresholdValue,
}
);
}
connect(): void {
this.intersectionObserver.observe(this.triggerTarget);
}
disconnect(): void {
this.intersectionObserver.unobserve(this.triggerTarget);
}
async fetchEntries(): Promise<void> {
this.page += 1;
if (this.page > this.pageCountValue) {
return;
}
this.element.classList.add(this.loadingClass);
let [response] = await Promise.allSettled([
fetch(`${this.endpointUrlValue}?page=${this.page}`, {
headers: {
Accept: "application/json",
},
}),
// artificial delay for better ux
new Promise((resolve) => setTimeout(resolve, 600)),
]);
if (response.status === "fulfilled") {
const data = await response.value.json();
this.entriesTarget.insertAdjacentHTML("beforeend", data.entries);
}
this.element.classList.remove(this.loadingClass);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment