Suodata viiva edistys (ja päivämäärän vaihteluväli Shenanigans) (Suomi (Finnish))

Suodata viiva edistys (ja päivämäärän vaihteluväli Shenanigans)

Monday, 10 November 2025

//

10 minute read

Johdanto

Viime päivinä olen takonut uutta blogin suodatinpalkkia: kielivalintaa, tilausta ja treffivalikoiman poimijaa, joka yrittää kovasti olla fiksu (toisinaan liian fiksu).

Tämä viesti kulkee läpi sen, mitä rakensin, miksi muutama asia meni pieleen (päivät eivät ole käytöstapoja, kohokohdat eivät ole virkistäviä) ja miten korjasin ne.

Paljon lähdekoodia ja pari merenneitokaaviota, jotka näyttävät virtauksen.

Muista, että tämä sivusto on työn alla, tällaista tulee tapahtumaan, kun syöt omaa koiranruokaasi!

  • Mikä muuttui?
  • Tässä on korkea taso: Lisäsin blogi-indeksiin oikean suodatinpalkin, joka sisältää:
  • Kielivalinta, joka säilyttää nykyiset suodattimet ja pyytää listan uudelleen HTMX:n kautta
  • Lajittele tilausvalinta (päiväys/otsikko asc/desc), joka myös säilyttää kontekstin
  • Päivämäärävalikoiman poimija (Flatpicker in range -tilassa) ja palvelinvetoiset päivän kohokohdat

URL-tila pidetään aina ajan tasalla (takaisin- ja eteenpäin-työt, syvien linkkien työ)

  • Palvelimen välimuisti vaihtelee nyt oikein suodattimen parametrien mukaanUutta tässä päivityksessä (The Big Fix!)
  • Näkyvyyssuodattimet: Viestit voidaan nyt piilottaa, julkaista tulevaisuudessa tai laittaa huipulle/blog/date-rangePäivämäärän päätetapahtuma
  • : UusiAPI palauttaa min/max-päivämäärät järkeville poimijarajoille<clear-param>Tyhjennä paramitagien apuri

: Helppo tapa tyhjentää suodattimet

lapussa auttaja Alpine.js:n kanssa

Matkan varrella minulla oli muutamia vikoja: treffien vaihteluvälit katosivat kielen ja tilausten vaihtuessa, kalenterin kohokohtia ei virkistänyt vaihtamalla, ja hauska, jossa pimeän tilan tyylit saivat kalenterin näyttämään rikki, koska Flatpicker ei piirtänyt sitä uudelleen.

<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>

Suodatinpalkki DOM:ssa

Tämä on karkea rakenne, jota skripti odottaa:Mostlylucid/src/js/blog-index.jsAsiakas: HTMX + Flatpicker-johdot

  • Käyttäytymisen ydin elää
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();
  • Muutama tärkeä kappale:
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);
    }
  }
});
  • URL-osoitteiden säilyttäminen ja HTMX-navigointi
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);
});
  • Alustus, oletus URL-osoitteesta ja UI-yhteenvedon pitäminen synkronoituna
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'] });

Flatpicker-valikoimassa palvelinvetoisia päivän kohokohtia

Säilöntäpäivät vaihdettaessa kieltä tai tilausta

  • Piirrä kalenteri uudelleen, kun teema tummuu (pimeä/kevyt) niin Flatpicker maalaa uudelleen*
  • Palvelin: päätetapahtumat ja välimuistitcalendar-daysSivulla on kolme päätepistettä:
  • Itse hakemisto, joka hyväksyy sivun, sivunKoko, aloitusPäivitä/loppuPäivitä, kieli ja tilausdate-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")
    });
}

päätetapahtuma, joka palauttaa tietyn kuukauden päivät, joissa on virkoja (koosteissa)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; }
}

päätetapahtuma, joka palauttaa min/max-päivämäärät kaikkien viestien osalta (kielitietoisuus)

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

Nopea sivusta: hakumalli on

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]

niin, että paginointi säilyttää nykyisen suodattimen kontekstin palvelimen puolella.

Sarja: miten suodatin vaihtuu

Flow: kalenterin kohokohtiaBlogPostEntity:

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

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

Uudet ominaisuudet: Näkyvyys ja sisällönhallintaBlogServicePost Näkyvyyssuodattimet

// 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);
}

Suurin parannus tässä päivityksessä on oikea jälkinäkyvyyden hallinta.

  • Kolme uutta kenttää lisättiin (IsHidden = trueErytropoietiini
  • **nyt suodattimet postaavat sopivasti:**Tämä tarkoittaa:ScheduledPublishDate
  • Piilopaikat (IsPinned = true) ei koskaan näy listauksissa

Aikataulutetut virat

Ilmestyy vasta heidän jälkeensä

[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 ...
}

Poistetut virat

<!-- 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>

) esiintyy aina ensimmäisenä sivulla 1, täydellinen ilmoituksille tai esillä olevalle sisällöllewindow.queryParamClearerTyhjennä paramitagi-auttaja

**Uusi ASP.NET Core -tunnisteavustin tekee kyselyparametrien selvittämisen helpoksi:**Käyttö Razorin näkökulmissa:

Lippuavustin integroituu Alppien.js:iin

  • komponentti URL-manipulaation ja HTMX-swapien käsittelyyn.

    • HuomautusstartDate/endDate.
    • : Sekä hakutarkkailija että kyselyparametri selkeämpi ansaitsevat syvemmän tutkimisen.new URL(window.location.href)Kannan Alpine.js-komponentin ja tunnisteiden auttajakuviot mukaan lukien kaikki toteutustiedot tulevassa julkaisussa, jossa kerrotaan uudelleenkäytettävistä HTMX-/Alpine-komponenteista.language/orderÖtököitä, joihin osuin (ja korjauksia)
  • Kieli-/järjestysmuutoksella menetetty päivämäärä

    • Syy: Rakensin uuden URL-osoitteen tyhjästä, en nykyisestä; hukassa
    • Korjaa: Aloita aina
  • ja vaihtaa vain ne osat, jotka ovat muuttuneet, tai lukea on peräisin Flatpicker jos läsnä.

    • Katso
    • edellä olevat vaihtokäsittelijät.<html class>Kalenterin kohokohdat eivät päivittyneet HTMX-vaihdoksen jälkeenfp.redraw().
  • Syy: Flatpicker-instance eli vanhalla DOM:lla ja kuoli vaihtokaupassa.

    • Korjata: On init, tuhota kaikki olemassa olevat installaatiot, luoda uudelleen, sitten esiladata kohokohtia näkyvälle kuukaudelle.LinkUrlMyös uudelleenkiinnitys swapien jälkeen (ks. alla).
    • Pimeä tila sai kalenterin näyttämään rikkinäiseltäposts.LinkUrl = Url.Action("Index","Blog", new { startDate, endDate, language, orderBy, orderDir })Syy: tyylikurssit vaihtuivat, mutta Flatpicker ei piirtänyt maalattuja päiviä uudelleen.

Korjaa: tarkkaile

muutoksia ja soittoa

(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);
})();

Paginaatiolinkit pudottivat suodattimen kontekstin

  • Syy:
  • Joissain tapauksissa kyselyä ei ollut tarkoitus ottaa mukaan.
  • Korjata: Aseta palvelimelle

niinpä hakukone säveltää URL-osoitteet oikein.

Finding related posts...
logo

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