Filter Balkenfortschritt und der Datumsbereich Shenanigans
Einleitung
In den letzten Tagen I' hämmerte weg an der neuen Blog-Filterbar .: ? Sprachauswahl . , ? Bestellung ?, ? und ein Datum Bereich Picker, die sehr hart versucht, smart zu sein ? ( ? manchmal auch smart ?). ? Dieser Beitrag geht durch das, was ich gebaut ? , ? warum ein paar Dinge ging seitwärts ? - ( ?Dates nicht berasieren ?
Erinnern Sie sich an diesen Aufstellungsort ist eine Arbeit im Fortschritt diese Art von Sachen, die geschehen wird, wenn Sie essen
Was hat sich verändert?
Hier ist das Hoch-‑. Level, ich habe einen richtigen Filterbalken zum Blogindex hinzugefügt, der auch das : enthält.
- Sprache auswählen, die Ihre aktuellen Filter bewahrt und Re---Anfragen an die Liste via HTMX
- Sortierung sortieren Wählen Sie (date" aus, das auch den Kontext bewahrt.
- Ein Datumsbereichspicker im Bereichsmodus mit server-angetriebenen Tageshighlights
- URL-Zustand wird immer in Sync gehalten.(.back./.forward works. ,. deep links work.MS K3.
- Server-Caching variiert jetzt korrekt durch die Filterparameter
Neu in diesem Update Die große Fixierung
- SichtfilterBeiträge können jetzt ausgeblendet werden ,, , geplant für die zukünftige Publikation , , , oder nach oben gepinnt
- Datumsbereich EndpunktNeu
/blog/date-rangeAPI liefert min./-max Daten für sinnvolle Picker-Grenzen zurück - Param Tag-Helfer löschenEinfacher Weg, um Filter zu löschen
<clear-param>Tag-Helfer mit Alpine-.-Js
Entlang des Weges hatte ich ein paar Bugs, die auf Sprache verloren gingen./.Order change.MSC2. Kalender-Highlights nicht erfrischend auf Swap und eine lustige, wo Dark Mode Stile den Kalender kaputt aussehen lassen, weil Flatpickr war...
Die Filterleiste im DOM
Dies ist die grobe Struktur, die das Skript erwartet.
<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>
Client-Verdrahtung mit Flatpickern
Kern des Verhaltens lebt in Mostlylucid/src/js/blog-index.jsEin paar wichtige Stücke
- URL-Konservierung und HTMX-Navigation
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();
}
}
- Initialisierung-Standardierung von URL-, und Beibehaltung der Zusammenfassung UI in Sync
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();
- Flatpickr Range mit server-angetriebenen Tageslichtern
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);
}
}
});
- Datum beim Ändern der Sprache oder Bestellung speichern
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);
});
- Re---Ziehen Kalender, wenn das Thema umschaltet (-Dark-/-Licht, so Flatpicker repaints
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'] });
Server-Endpunkte und Caching
Drei Endpunkte versorgen die Seite
- Der Index selbst, der die Seite akzeptiert.*
- A
calendar-daysEndpunkt, der den Satz von Tagen in einem bestimmten Monat zurückgibt, die Posts haben. - A
date-rangeEndpunkt, der die Min-/-max-Daten über alle Beiträge zurückgibt
// 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")
});
}
Ein schnelles beiseite-:- das Paging-Modell hat LinkUrl so dass die Paginierung den aktuellen Filterkontext bei gerendertem Server bewahrt
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; }
}
Sequenz: :, wie ein Filter sich verändert
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
Flow-Highlights für den MSK-Kalender
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]
Neue Funktionen – Sichtbarkeit und Content Control
Nachsichtsfilter
Die größte Verbesserung in diesem Update ist die richtige Post-Opportunity-Management-. BlogPostEntity:
public class BlogPostEntity
{
// ... existing fields ...
public bool IsPinned { get; set; }
public bool IsHidden { get; set; }
public DateTimeOffset? ScheduledPublishDate { get; set; }
}
Das BlogService Filtert jetzt Beiträge entsprechend
// 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);
}
Das bedeutet:
- Versteckte Stellen (
IsHidden = trueNicht in Auflistungen erscheinen - Planstellen nur nach ihrer
ScheduledPublishDate - Geheftete Stellen (
IsPinned = trueImmer zuerst auf der Seite erscheinen, perfekt für Ankündigungen oder vorgestellte Inhalte
Param Tag Helfer löschen
Ein neuer ASP-Tag Helper macht es einfach, Abfrageparameter zu löschen.
[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 ...
}
Verwendung in Razor-Ansichten
<!-- 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>
Der Tag-Helfer integriert sich mit Alpine-..js.' window.queryParamClearer Komponente, um die URL-Manipulation und HTMX Swaps zu handhaben
AnmerkungSowohl der Paging-Tag-Helper als auch der Query-Parameter verdienen eine tiefere Exploration... IM'.ll deckt die vollständigen Implementierungsdetails ab, einschließlich der Alpine-..js-Komponente und Tag-Helfermuster in einem zukünftigen Beitrag zum Bau von wiederverwendbaren HTMX./.Alpine-Komponenten.
Fehler, die ich traf und die Korrekturen
Datumsbereich auf Sprache verloren . . / .
- Ursache: Ich habe die neue URL von Grund auf gebaut.
startDate/endDate. - Beheben Sie immer mit beginnen
new URL(window.location.href)und ändern Sie nur die Teile, die geändert werden, ;- oder Lesedaten von Flatpickr, wenn vorhandenlanguage/orderÄnderungshandler über dem .
- Ursache: Ich habe die neue URL von Grund auf gebaut.
Kalender-Highlights wurden nach einem HTMX-Swap aktualisiert
- Ursache: Die Flatpickr-Instanz lebte auf der alten DOM und starb am Swap.
- Fixieren Sie auf init-,. Zerstören Sie alle vorhandenen Instanzen.,. re...‑.create.,. dann Vorspann-Highlights für den sichtbaren Monat!..
Dunkler Modus ließ den Kalender kaputt aussehen
- Ursache: Die Stilklassen haben sich geändert, aber Flatpickr hat die gemalten Tage neu gezeichnet
- Beobachten
<html class>Änderungen und Anruffp.redraw().
Pagination Links gelöscht Filter Kontext
- Ursache
LinkUrlWas?’?t gesetzt, um die Abfrage in einigen Fällen enthalten? - Fixieren von :-Daten Auf dem Server-Set
posts.LinkUrl = Url.Action("Index","Blog", new { startDate, endDate, language, orderBy, orderDir })so dass der Pager URLs richtig komponiert
- Ursache
HTMX-Swapsicherheit – :-Re---Init auf Anfrage
Wenn du partielle Swaps verwendest, muss dein JS idempotent sein, .. In meinem Fall wickle ich alles um und drehe init nach einem Swap um.:.
(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);
})();
Was kommt als nächstes?
- Fügen Sie schnelle Voreinstellungen hinzu Letzte Tage des 7, dieses Jahr des)
- Persistische Sprache in localStorage, um die Seite zu speichern
- Tastatur-Gebühren für den Datumsbereich
Wenn Sie etwas Seltsames mit den Filtern vorfinden... bitte hinterlassen Sie einen Kommentar mit Ihrem Browser.