Avanzamento della barra del filtro (e la gamma di date Shenanigan) (Italiano (Italian))

Avanzamento della barra del filtro (e la gamma di date Shenanigan)

Monday, 10 November 2025

//

12 minute read

Introduzione

Negli ultimi giorni sono stato a martellare alla nuova barra dei filtri del blog: selezione della lingua, ordinazione, e un selezionatore di date che cerca molto difficile per essere intelligente (a volte troppo intelligente).

Questo post passa attraverso ciò che ho costruito, perché alcune cose sono andate di lato (date non comportarsi, highlights non rinfrescante), e come li ho riparati.

Un sacco di codice sorgente e un paio di diagrammi di sirena per mostrare il flusso.

Ricordate questo sito è un lavoro in corso, questo genere di cose accadranno quando si mangia-il vostro-proprio-cibo da cane!

  • Cosa e' cambiato?
  • Ecco l'alto livello: ho aggiunto una barra filtrante corretta all'indice del blog che include:
  • Lingua selezionata che conserva i filtri attuali e richiede nuovamente l'elenco tramite HTMX
  • Ordina l'ordine seleziona (data/titolo asc/desc) che conserva anche il contesto
  • Un selettore dell'intervallo di date (Flatpickr in modalità di intervallo) con evidenziazioni giornaliere basate sul server

Lo stato dell'URL è sempre mantenuto in sincronia (lavori back/forward, lavoro collegamenti profondi)

  • La cache del server ora varia correttamente dai parametri del filtroNuovo in questo aggiornamento (The Big Fix!)
  • Filtri di visibilità: I post possono ora essere nascosti, programmati per la pubblicazione futura, o appuntati alla parte superiore/blog/date-rangeEndpoint dell'intervallo di date
  • : NuovoAPI restituisce le date min/max per i limiti sensibili del prelievo<clear-param>Pulisci l'helper del tag param

: Semplice modo per cancellare i filtri utilizzando

Tag helper con Alpine.js

Lungo il percorso ho avuto alcuni bug: intervalli di date da perdere sul cambiamento di lingua/ordine, evidenziazioni calendario non rinfrescante sullo swap, e uno divertente in cui gli stili di modalità scura ha fatto il calendario sembrare rotto perché Flatpickr non era ri-disegno.

<div id="filters" hx-target="#content">
  <select id="languageSelect">…</select>
  <select id="orderSelect">
    <option value="date_desc">Newest first</option>
    <option value="date_asc">Oldest first</option>
    <option value="title_asc">Title A–Z</option>
    <option value="title_desc">Title Z–A</option>
  </select>
  <input id="dateRange" type="text" placeholder="YYYY-MM-DD → YYYY-MM-DD" />
  <button id="clearDateFilter">Clear</button>
  <div id="filterSummary"></div>
</div>

<div id="content"><!-- HTMX swaps blog list here --></div>

La barra dei filtri nel DOM

Questa è la struttura approssimativa che lo script si aspetta:Mostlylucid/src/js/blog-index.jsCliente: Cablaggio HTMX + Flatpickr

  • Il nucleo del comportamento vive in
function applyNavigation(u){
  const target = document.querySelector('#content');
  try{ window.history.pushState({}, '', u.toString()); }catch{}
  if(window.htmx && target){
    window.htmx.ajax('GET', u.toString(), {
      target: '#content',
      swap: 'outerHTML show:none',
      headers: {'pagerequest': 'true'}
    });
  } else {
    window.location.href = u.toString();
  }
}
const url = new URL(window.location.href);
const existingStart = url.searchParams.get('startDate');
const existingEnd   = url.searchParams.get('endDate');
const existingLang  = url.searchParams.get('language') || 'en';
const existingOrderBy = (url.searchParams.get('orderBy') || 'date').toLowerCase();
const existingOrderDir = (url.searchParams.get('orderDir') || 'desc').toLowerCase();

langSelect.value = existingLang;
orderSelect.value = `${existingOrderBy}_${existingOrderDir}`;
updateSummary();
  • Alcuni pezzi importanti:
async function fetchMonth(year, month, language){
  const res = await fetch(`/blog/calendar-days?year=${year}&month=${month}&language=${encodeURIComponent(language||'en')}`);
  if(!res.ok) return new Set();
  const j = await res.json();
  return new Set(j.dates || []);
}

function formatYMD(d){ return d.toISOString().substring(0,10); }

let highlightDates = new Set();
const fp = window.flatpickr(input, {
  mode: 'range',
  dateFormat: 'Y-m-d',
  defaultDate: [existingStart, existingEnd].filter(Boolean),
  onDayCreate: function(_dObj,_dStr,fpInstance,dayElem){
    const ymd = formatYMD(dayElem.dateObj);
    if(highlightDates.has(ymd)){
      dayElem.classList.add('has-post');
      dayElem.style.background = 'rgba(76,175,80,0.35)';
      dayElem.style.borderRadius = '6px';
    }
  },
  onMonthChange: async function(_sd,_ds,fpInstance){
    highlightDates = await fetchMonth(fpInstance.currentYear, fpInstance.currentMonth+1, langSelect.value);
    fpInstance.redraw();
  },
  onOpen: async function(_sd,_ds,fpInstance){
    highlightDates = await fetchMonth(fpInstance.currentYear, fpInstance.currentMonth+1, langSelect.value);
    fpInstance.redraw();
  },
  onChange: function(selectedDates){
    if(selectedDates.length === 2){
      const [start,end] = selectedDates;
      const u = new URL(window.location.href);
      u.searchParams.set('startDate', formatYMD(start));
      u.searchParams.set('endDate', formatYMD(end));
      u.searchParams.set('page','1');
      u.searchParams.set('language', langSelect.value);
      const [ob,od] = orderSelect.value.split('_');
      u.searchParams.set('orderBy', ob);
      u.searchParams.set('orderDir', od);
      updateSummary();
      applyNavigation(u);
    }
  }
});
  • Conservazione URL e navigazione HTMX
langSelect.addEventListener('change', async function(){
  const u = new URL(window.location.href);
  u.searchParams.set('language', langSelect.value);
  u.searchParams.set('page', '1');
  const [ob,od] = (orderSelect.value||'date_desc').split('_');
  u.searchParams.set('orderBy', ob);
  u.searchParams.set('orderDir', od);
  if(input._flatpickr && input._flatpickr.selectedDates.length===2){
    const [s,e] = input._flatpickr.selectedDates;
    u.searchParams.set('startDate', formatYMD(s));
    u.searchParams.set('endDate', formatYMD(e));
  }
  // refresh calendar highlights for new language
  if(input._flatpickr){
    const fp = input._flatpickr;
    highlightDates = await fetchMonth(fp.currentYear, fp.currentMonth+1, langSelect.value);
    fp.redraw();
  }
  updateSummary();
  applyNavigation(u);
});
  • Inizializzazione, default da URL e mantenimento dell'interfaccia utente sintetica in sincronizzazione
const obs = new MutationObserver(() => {
  const dr = root.querySelector('#dateRange');
  const fp = dr && dr._flatpickr;
  if(fp && typeof fp.redraw === 'function') fp.redraw();
});
obs.observe(document.documentElement, { attributes:true, attributeFilter:['class'] });

Gamma Flatpickr con momenti di puntamento del giorno guidati dal server

Preserva le date quando si cambia lingua o ordine

  • Ri-disegna il calendario quando il tema commuta (scuro/leggero) in modo Flatpickr rivernicia*
  • Server: endpoint e cachecalendar-daysTre endpoint alimentano la pagina:
  • L'indice stesso, che accetta page, pageSize, startDate/endDate, language, and orderdate-rangeA
// GET /blog
[HttpGet]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request",
  VaryByQueryKeys = new[] { "page", "pageSize", nameof(startDate), nameof(endDate), nameof(language), nameof(orderBy), nameof(orderDir) },
  Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request" },
  VaryByQueryKeys = new[] { nameof(page), nameof(pageSize), nameof(startDate), nameof(endDate), nameof(language), nameof(orderBy), nameof(orderDir) })]
public async Task<IActionResult> Index(int page = 1, int pageSize = 20, DateTime? startDate = null, DateTime? endDate = null,
  string language = MarkdownBaseService.EnglishLanguage, string orderBy = "date", string orderDir = "desc")
{
    var posts = await blogViewService.GetPagedPosts(page, pageSize, language: language, startDate: startDate, endDate: endDate);
    posts.LinkUrl = Url.Action("Index", "Blog", new { startDate, endDate, language, orderBy, orderDir });
    if (Request.IsHtmx()) return PartialView("_BlogSummaryList", posts);
    return View("Index", posts);
}

// GET /blog/calendar-days?year=2025&month=11&language=en
[HttpGet("calendar-days")]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request", VaryByQueryKeys = new[] { nameof(year), nameof(month), nameof(language) }, Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 1800, VaryByHeaderNames = new[] { "hx-request" }, VaryByQueryKeys = new[] { nameof(year), nameof(month), nameof(language) })]
public async Task<IActionResult> CalendarDays(int year, int month, string language = MarkdownBaseService.EnglishLanguage)
{
    if (year < 2000 || month < 1 || month > 12) return BadRequest("Invalid year or month");
    var start = new DateTime(year, month, 1);
    var end = start.AddMonths(1).AddDays(-1);
    var posts = await blogViewService.GetPostsForRange(start, end, language: language);
    if (posts is null) return Json("");
    var dates = posts.Select(p => p.PublishedDate.Date).Distinct().OrderBy(d => d).Select(d => d.ToString("yyyy-MM-dd")).ToList();
    return Json(new { dates });
}

// GET /blog/date-range?language=en
[HttpGet("date-range")]
[ResponseCache(Duration = 3600, VaryByHeader = "hx-request", VaryByQueryKeys = new[] { nameof(language) }, Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 7200, VaryByHeaderNames = new[] { "hx-request" }, VaryByQueryKeys = new[] { nameof(language) })]
public async Task<IActionResult> DateRange(string language = MarkdownBaseService.EnglishLanguage)
{
    var allPosts = await blogViewService.GetAllPosts();
    if (allPosts is null || !allPosts.Any())
    {
        return Json(new
        {
            minDate = DateTime.UtcNow.AddYears(-1).ToString("yyyy-MM-dd"),
            maxDate = DateTime.UtcNow.ToString("yyyy-MM-dd")
        });
    }

    var posts = allPosts;
    if (!string.IsNullOrEmpty(language) && language != MarkdownBaseService.EnglishLanguage)
    {
        posts = allPosts.Where(p => p.Language.Equals(language, StringComparison.OrdinalIgnoreCase)).ToList();
    }

    if (!posts.Any()) posts = allPosts;

    var minDate = posts.Min(p => p.PublishedDate.Date);
    var maxDate = posts.Max(p => p.PublishedDate.Date);

    return Json(new
    {
        minDate = minDate.ToString("yyyy-MM-dd"),
        maxDate = maxDate.ToString("yyyy-MM-dd")
    });
}

endpoint che restituisce l'insieme di giorni in un determinato mese che hanno post (per evidenziazioni)LinkUrlA

public class BasePagingModel<T> : Interfaces.IPagingModel<T> where T : class
{
    public int Page { get; set; }
    public int TotalItems { get; set; } = 0;
    public int PageSize { get; set; }
    public ViewType ViewType { get; set; } = ViewType.TailwindAndDaisy;
    public string LinkUrl { get; set; }
    public List<T> Data { get; set; }
}

endpoint che restituisce le date min/max in tutti i post (language-aware)

sequenceDiagram
  participant U as User
  participant FP as Flatpickr
  participant JS as blog-index.js
  participant HT as HTMX
  participant C as BlogController

  U->>FP: Selects 2025-11-01 → 2025-11-10
  FP-->>JS: onChange([start,end])
  JS->>JS: Update URLSearchParams (startDate, endDate, language, order)
  JS->>HT: htmx.ajax('GET', /blog?...)
  HT->>C: GET /blog with query
  C-->>HT: Partial _BlogSummaryList
  HT-->>U: Swap #content
  JS->>FP: (after swap) ensure highlights + redraw

Un rapido da parte: il modello di Paging ha

flowchart TD
  A[Open calendar / Month change] --> B[Fetch /blog/calendar-days]
  B -->|JSON dates: yyyy-mm-dd array| C[Set highlightDates]
  C --> D[flatpickr redraw]
  D --> E[onDayCreate adds .has-post]

in modo che la paginazione conservi il contesto del filtro corrente quando viene visualizzato il server-side.

Sequenza: come un filtro cambia i flussi

Flusso: evidenziazioni del calendarioBlogPostEntity:

public class BlogPostEntity
{
    // ... existing fields ...

    public bool IsPinned { get; set; }
    public bool IsHidden { get; set; }
    public DateTimeOffset? ScheduledPublishDate { get; set; }
}

Nuove funzionalità: Visibilità e Controllo dei ContenutiBlogServiceFiltri di visibilità post

// Filter out hidden posts and posts scheduled for the future
var now = DateTimeOffset.UtcNow;
postQuery = postQuery.Where(x =>
    !x.IsHidden &&
    (x.ScheduledPublishDate == null || x.ScheduledPublishDate <= now));

// For page 1, prioritize pinned posts
var isFirstPage = page == null || page.Value == 1;
if (isFirstPage)
{
    postQuery = postQuery.OrderByDescending(x => x.IsPinned)
                         .ThenByDescending(x => x.PublishedDate.DateTime);
}

Il più grande miglioramento in questo aggiornamento è la corretta gestione della visibilità post.

  • Sono stati aggiunti tre nuovi campi (IsHidden = trueLa
  • **ora filtra i messaggi in modo appropriato:**Ciò significa:ScheduledPublishDate
  • Posti nascosti (IsPinned = true) mai apparire in elenchi

Posti programmati

appaiono solo dopo la loro

[HtmlTargetElement("clear-param")]
public class ClearParamTagHelper : TagHelper
{
    [HtmlAttributeName("name")]
    public string? Name { get; set; }

    [HtmlAttributeName("all")]
    public bool All { get; set; } = false;

    [HtmlAttributeName("exclude")]
    public string Exclude { get; set; } = "";

    // ... styling and Alpine.js integration ...
}

Posti bloccati

<!-- Clear a specific parameter -->
<clear-param name="startDate">Clear Date</clear-param>

<!-- Clear all parameters except language -->
<clear-param all="true" exclude="language">Clear All Filters</clear-param>

) appaiono sempre per primi a pagina 1, perfetti per annunci o contenuti in evidenzawindow.queryParamClearerElimina l'helper del tag param

**Un nuovo ASP.NET Core tag helper consente di cancellare facilmente i parametri della query:**Uso nelle viste Razor:

Il tag helper si integra con Alpine.js

  • componente per gestire la manipolazione URL e gli swap HTMX.

    • NotastartDate/endDate.
    • : Sia l'helper del tag Paging che il parametro della query più chiaro meritano un'esplorazione più profonda.new URL(window.location.href)Coprirò tutti i dettagli di implementazione, tra cui il componente Alpine.js e tag helper pattern, in un futuro post sulla costruzione di componenti riutilizzabili HTMX/Alpine.language/orderBugs che ho colpito (e le correzioni)
  • Intervallo di data perso in caso di cambiamento della lingua/dell'ordine

    • Causa: Ho costruito il nuovo URL da zero, non da quello attuale; perso
    • Correggi: inizia sempre con
  • e solo cambiare le parti che hanno cambiato; o leggere le date da Flatpickr se presente.

    • Vedere il
    • i soggetti che operano con il cambiamento di cui sopra.<html class>Calendar highlights didn't refresh dopo uno swap HTMXfp.redraw().
  • Causa: L'istanza Flatpickr viveva sul vecchio DOM e morì in cambio.

    • Fissare: Init, distruggere qualsiasi istanza esistente, ri-creare, quindi precaricare i punti salienti per il mese visibile.LinkUrlAnche ri-gancio dopo swap (cfr. sotto).
    • La modalità scura ha fatto sembrare il calendario rottoposts.LinkUrl = Url.Action("Index","Blog", new { startDate, endDate, language, orderBy, orderDir })Causa: le classi di stile cambiato ma Flatpickr didn didn diredraw giorni dipinti.

Correggi: Osserva

modifiche e chiamata

(function(){
  function initFromRoot(root){ /* …the big function shown above… */ }
  if(window.htmx){
    document.body.addEventListener('htmx:afterSwap', function(ev){
      const tgt = ev.detail.target;
      // re-init if the content swapped includes the filters/content area
      if(tgt && (tgt.id === 'content' || tgt.querySelector?.('#filters'))){
        initFromRoot(document);
      }
    });
  }
  // also run on first load
  initFromRoot(document);
})();

Il contesto del filtro dei collegamenti di paginazione è caduto

  • Causa:
  • in alcuni casi non è stata impostata l'inclusione della query.
  • Corretto: Sul server, impostare

così il pager compone correttamente gli URL.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.