A karcsú animációk koordinálása az XState-tel

Forrás csomópont: 806500

Ez a bejegyzés egy bevezető XState ahogyan az a Karcsú projekt. Az XState egyedülálló a JavaScript ökoszisztémában. Nem tartja szinkronban a DOM-ot az alkalmazás állapotával, de segít az alkalmazás állapotának kezelésében, mivel lehetővé teszi, hogy véges állapotú gépként (FSM) modellezze.

Az állapotgépek és a formális nyelvek mélyreható elmélyülése meghaladja ennek a bejegyzésnek a kereteit, de Jon Bellah ezt teszi egy másik CSS-Tricks cikkben. Egyelőre gondoljon az FSM-re folyamatábraként. A folyamatábrák számos állapotot tartalmaznak, amelyeket buborékok és nyilak mutatnak, amelyek egyik állapotból a másikba vezetnek, jelezve az egyik állapotból a másikba való átmenetet. Az állapotgépeken egynél több nyíl vezethet ki egy állapotból, vagy egy sem, ha az egy végső állapot, sőt, lehetnek nyilak, amelyek elhagyják az állapotot, és közvetlenül ugyanabba az állapotba mutatnak.

Ha mindez lehengerlőnek hangzik, lazíts, szépen és lassan belemegyünk minden részletbe. Egyelőre a magas szintű nézet az, hogy amikor az alkalmazásunkat állapotgépként modellezzük, akkor különböző „állapotokat” fogunk létrehozni, amelyekben az alkalmazásunk lehet (értsd… állapotgép… állapotok?), és a bekövetkező eseményeket. és az állapotváltozásokat okozó nyilak az állapotok között lesznek. Az XState az állapotokat „állapotoknak”, az állapotok közötti nyilakat pedig „cselekvéseknek” nevezi.

A mi példánk

Az XState tanulási görbéje van, ami kihívást jelent a tanításban. Túl mesterkélt használati esettel szükségtelenül bonyolultnak tűnik. Csak akkor ragyog az XState, ha egy alkalmazás kódja kissé összekuszálódik. Ez bonyolulttá teszi az írást. Ennek ellenére a példa, amelyet megvizsgálunk, egy automatikus kiegészítõ widget (néha autosuggestnek is nevezik), vagy egy beviteli doboz, amelyre kattintva megjelenik a választható elemek listája, és a bevitel beírása közben melyik szűrő.

Ebben a bejegyzésben megvizsgáljuk az animációs kód tisztítását. Íme a kiindulópont:

Ez a valódi kód az enyémtől karcsú-segítők könyvtárban, bár a bejegyzéshez eltávolították a felesleges részeket. Kattinthat a bevitelre, és szűrheti az elemeket, de nem tud semmit kiválasztani, „lefelé mutató nyilat” az elemek között, az egérmutatót stb.

Megnézzük az elemek listájának animációját. Amikor rákattint a bemenetre, és az eredménylista először megjelenik, le akarjuk animálni. A gépelés és a szűrés során a lista méreteinek változásai egyre kisebbek és nagyobbak lesznek. És amikor a bemenet elveszíti a fókuszt, vagy rákattint ESC, a lista magasságát nullára animáljuk, miközben elhalványítjuk, majd eltávolítjuk a DOM-ból (és nem korábban). Hogy a dolgok érdekesebbek (és kellemesebbek legyenek a felhasználó számára), használjunk más rugós konfigurációt a nyitáshoz, mint amit a záráshoz használunk, így a lista valamivel gyorsabban vagy merevebben záródik, így a felesleges UX nem marad el rajta. túl hosszú a képernyő.

Ha kíváncsi arra, hogy miért nem használom a Svelte átmeneteket a DOM-on belüli és a DOM-on kívüli animációk kezelésére, ez azért van, mert a lista méreteit is animálom, amikor az nyitva van, ahogy a felhasználó szűri, és koordinálom az átmenet és a normál között. A tavaszi animációk sokkal nehezebb, mint egyszerűen megvárni a tavaszi frissítést, hogy nullára érjen, mielőtt eltávolítana egy elemet a DOM-ból. Például mi történik, ha a felhasználó gyorsan beírja és szűri a listát, miközben az animál? Amint látni fogjuk, az XState egyszerűvé teszi az ehhez hasonló bonyolult állapotátmeneteket.

A probléma körének meghatározása

Vessünk egy pillantást a kódra a példa eddig. Van egy open változót, amellyel vezérelhető, amikor a lista nyitva van, és a resultsListVisible tulajdonság annak szabályozására, hogy a DOM-ban kell-e lennie. Nálunk is van a closing változó, amely azt szabályozza, hogy a lista bezárása folyamatban van-e.

A 28-as vonalon van egy inputEngaged metódus, amely akkor fut le, amikor a bemenetre kattintanak vagy fókuszálnak. Egyelőre jegyezzük meg, hogy beállítja open és a resultsListVisible igazra. inputChanged akkor hívódik meg, amikor a felhasználó beírja a bemenetet, és beállítja open igaznak. Ez arra szolgál, hogy amikor a bevitel fókuszálva van, a felhasználó az Escape billentyűvel bezárja, majd elkezd gépelni, így újra megnyílik. És természetesen a inputBlurred függvény akkor fut le, amikor az elvárható, és beállítja closing igaz, és open hamisra.

Szedjük szét ezt a kusza összevisszaságot, és nézzük meg, hogyan működnek az animációk. Jegyezze meg a slideInSpring és a opacitySpring a csúcson. Az előbbi felfelé és lefelé csúsztatja a listát, és beállítja a méretet, ahogy a felhasználó gépel. Ez utóbbi eltünteti a listát, ha elrejti. Leginkább a slideInSpring.

Vessen egy pillantást az ún. függvény szörnyűségére setSpringDimensions. Ez frissíti a csúszórugónkat. A fontos darabokra összpontosítva veszünk néhány logikai tulajdonságot. Ha a lista nyílik, akkor beállítjuk a nyitó rugós konfigurációt, azonnal beállítjuk a lista szélességét (akarom, hogy a lista csak lefelé csússzon, ne lefelé és kifelé), a { hard: true } config, majd állítsa be a magasságot. Ha bezárunk, nullára animálunk, és amikor az animáció befejeződött, beállítjuk resultsListVisible hamisra (ha a záró animáció megszakad, Svelte elég okos lesz ahhoz nem teljesítse az ígéretet, így a visszahívás soha nem fut le). Végül ezt a metódust is hívják minden alkalommal, amikor a találati lista mérete megváltozik, azaz amikor a felhasználó szűr. Felállítottunk a ResizeObserver máshol kezelni ezt.

Rengeteg spagetti

Vegyük számba ezt a kódot.

  • Megvan a mi open változó, amely nyomon követi, ha a lista nyitva van.
  • Megvan a resultsListVisible változó, amely nyomon követi, hogy a listának a DOM-ban kell-e lennie (és hamisra állítva a bezárási animáció befejezése után).
  • Megvan a closing változó, amely nyomon követi, hogy a lista bezárás alatt van-e, amit a beviteli fókusz/kattintáskezelőben ellenőrizünk, így meg tudjuk fordítani a záró animációt, ha a felhasználó gyorsan újra bekapcsolja a widgetet, mielőtt befejezné a bezárást.
  • Nekünk szintén van setSpringDimensions hogy négy különböző helyen hívjuk. A rugóinkat aszerint állítja be, hogy a lista nyitás, zárás vagy éppen átméretezés folyamatban van nyitva (vagyis ha a felhasználó szűri a listát).
  • Végül van egy resultsListRendered Karcsú művelet, amely akkor fut le, amikor az eredménylista DOM-eleme megjelenik. Ez elindítja a miénket ResizeObserver, és amikor a DOM csomópont lecsatlakozik, beállítja closing hamisra.

Elkaptad a hibát? Amikor az ESC gomb megnyomva, csak állítok open nak nek false. Elfelejtettem beállítani a zárást true, és hívja setSpringDimensions(false, true). Ezt a hibát nem szándékosan találták ki ehhez a blogbejegyzéshez! Ez egy valódi hiba, amit elkövettem, amikor átdolgoztam a widget animációit. Csak be tudom másolni a kódot inputBlured oda, ahol a escape gomb elkapta, vagy akár áthelyezheti egy új funkcióba, és mindkét helyről hívhatja. Ezt a hibát alapvetően nem nehéz megoldani, de növeli a kód kognitív terhelését.

Sok mindent nyomon követünk, de ami a legrosszabb, ez az állapot szétszórva van a modulban. Vegye ki a fent leírt bármely állapotrészletet, és használja a CodeSandbox Keresés funkcióját az összes olyan hely megtekintéséhez, ahol az adott állapotrészletet használják. Látni fogja, hogy a kurzor a fájlon keresztül ugrál. Most képzeld el, hogy új vagy ebben a kódban, és megpróbálod értelmezni. Gondolj bele ezeknek az állapotelemeknek a növekvő mentális modelljére, amelyeket nyomon kell követned, és rájössz, hogyan működik az összes létező hely alapján. Mindannyian ott voltunk; ez szívás. Az XState jobb módszert kínál; lássuk hogyan.

Bemutatkozik az XState

Lépjünk hátrébb egy kicsit. Nem lenne egyszerűbb úgy modellezni a widgetünket, hogy milyen állapotban van, a felhasználó interakciója során bekövetkező eseményekkel, amelyek mellékhatásokat okoznak, és új állapotokba váltanak át? Persze, de ezt már csináltuk; a probléma az, hogy a kód mindenhol szét van szórva. Az XState lehetőséget ad arra, hogy ilyen módon megfelelően modellezzük állapotunkat.

Elvárások megfogalmazása

Ne várd, hogy az XState varázsütésre eltüntesse az összes komplexitásunkat. Még mindig össze kell hangolnunk a rugókat, módosítanunk kell a rugó konfigurációját a nyitási és zárási állapotok alapján, kezelnünk kell az átméretezéseket stb. Amit az XState ad nekünk, az az a képesség, hogy ezt az állapotkezelési kódot könnyen érthető módon központosítsuk és beállítsuk. Valójában a teljes sorszámunk egy kicsit növekedni fog az állapotgép beállításának eredményeként. Lássuk.

Az első állapotgéped

Ugorjunk be, és nézzük meg, hogyan néz ki egy csupasz csontozatú gép. Az XState FSM-csomagját használom, amely az XState minimális, lecsökkentett változata, apró, 1 KB-os kötegmérettel, amely tökéletes a könyvtárakhoz (mint például egy autosuggest widget). Nem rendelkezik sok olyan fejlett funkcióval, mint a teljes XState csomag, de a használati esetünkben nem is lenne rájuk szükségünk, és egy ilyen bevezető bejegyzéshez sem szeretnénk.

Állapotgépünk kódja lent található, és az interaktív demó véget ért a Code Sandboxnál. Sok van, de hamarosan kitérünk rá. És hogy világos legyen, még nem működik.

const stateMachine = createMachine( { initial: "initial", context: { open: false, node: null }, states: { initial: { on: { OPEN: "open" } }, open: { on: { RENDERED: { actions: "rendered" }, RESIZE: { actions: "resize" }, CLOSE: "closing" }, entry: "opened" }, closing: { on: { OPEN: { target: "open", actions: ["resize"] }, CLOSED: "closed" }, entry: "close" }, closed: { on: { OPEN: "open" }, entry: "closed" } } }, { actions: { opened: assign(context => { return { ...context, open: true }; }), rendered: assign((context, evt) => { const { node } = evt; return { ...context, node }; }), close() {}, resize(context) {}, closed: assign(() => { return { open: false, node: null }; }) } }
);

Menjünk fentről lefelé. A initial A tulajdonság szabályozza a kezdeti állapotot, amelyet én „kezdetinek” neveztem. context az állapotgépünkhöz kapcsolódó adat. Tárolok egy logikai értéket arra vonatkozóan, hogy az eredménylista jelenleg nyitva van-e, valamint a node objektum ugyanahhoz az eredménylistához. Ezután az állapotainkat látjuk. Minden állapot egy kulcs a states ingatlan. A legtöbb állam esetében látható, hogy van egy on ingatlan, és egy entry ingatlan.

on eseményeket konfigurál. Minden eseménynél áttérhetünk egy új állapotba; futtathatunk mellékhatásokat, úgynevezett cselekvéseket; vagy mindkettő. Például amikor a OPEN az esemény belül történik initial állapotba költözünk a open állapot. Amikor az RENDERED esemény történik a open állam, mi futtatjuk a rendered akció. És amikor a OPEN belül történik az esemény closing állapotba lépünk át a open állapotot, és futtassa az átméretezési műveletet is. A entry A legtöbb állapoton látható mező egy műveletet úgy konfigurál, hogy az állapot megadásakor automatikusan lefusson. Vannak még exit akciókat, bár itt nincs szükségünk rájuk.

Van még néhány dolog, amit meg kell vizsgálnunk. Nézzük meg, hogyan változhatnak állapotgépünk adatai vagy kontextusa. Ha azt akarjuk, hogy egy művelet a kontextust módosítsa, becsomagoljuk assign és visszaadjuk az új kontextust a cselekvésünkből; ha nincs szükségünk semmilyen feldolgozásra, akkor az új állapotot közvetlenül átadhatjuk assign. Ha a műveletünk nem frissíti a kontextust, azaz csak a mellékhatások miatt van, akkor nem csomagoljuk be az akciófüggvényünket assign, és csak olyan mellékhatásokat hajtsunk végre, amelyekre szükségünk van.

Állapotgépünk változásának befolyásolása

Van egy klassz modellünk az állapotgépünkhöz, de hogyan? futás azt? Használjuk a interpret funkciót.

const stateMachineService = interpret(stateMachine).start();

Most stateMachineService a futó állapotgépünk, amelyen eseményeket hívhatunk meg, hogy kikényszerítsük átmeneteinket és cselekvéseinket. Esemény elindításához hívunk send, átadja az esemény nevét, majd opcionálisan az eseményobjektumot. Például a Svelte műveletünkben, amely akkor fut le, amikor az eredménylista először felkerül a DOM-ba, ez van:

stateMachineService.send({ type: "RENDERED", node });

A megjelenített művelet így kapja meg az eredménylista csomópontját. Ha körülnézel a többi AutoComplete.svelte fájlt, látni fogja az összes ad hoc állapotkezelési kódot egysoros eseményküldésre cserélve. A bemeneti kattintás/fókusz eseménykezelőjében futtatjuk a OPEN esemény. A ResizeObserverünk elindítja a RESIZE esemény. Stb.

Álljunk meg egy pillanatra, és értékeljük azokat a dolgokat, amelyeket az XState itt ingyen ad nekünk. Nézzük meg azt a kezelőt, amely akkor fut, amikor az XState hozzáadása előtt a bemenetünkre kattintanak vagy fókuszálnak.

function inputEngaged(evt) { if (closing) { setSpringDimensions(); } open = true; resultsListVisible = true;
} 

Azelőtt ellenőriztük, hogy zárunk-e, és ha igen, kényszerítjük a csúszórugónk újraszámolását. Ellenkező esetben megnyitottuk a widgetünket. De mi történt, ha rákattintunk a bemenetre, amikor az már nyitva volt? Ugyanaz a kód futott újra. Szerencsére ez nem igazán számított. Svelte-t nem érdekli, ha újra beállítjuk open és a resultsListVisible a már birtokolt értékekhez. De ezek az aggodalmak az XState segítségével eltűnnek. Az új verzió így néz ki:


function inputEngaged(evt) { stateMachineService.send("OPEN");
}

Ha az állapotgépünk már nyitott állapotban van, és kigyújtjuk a OPEN esemény, akkor nem történik semmi, hiszen nincs OPEN az adott állapothoz konfigurált esemény. És az a speciális kezelés, amikor a bemenetre kattintanak, amikor az eredmények bezárulnak? Ezt is közvetlenül az állapotgép konfigurációja kezeli – figyelje meg, hogyan a OPEN esemény rátapint a resize művelet, amikor az a closing állapot.

És természetesen kijavítottuk a ESC kulcshiba korábban. Most a gomb megnyomása egyszerűen elindítja a CLOSE esemény, és ennyi.

Befejezés

A vége szinte antiklimatikus. El kell végeznünk az összes munkát, amit korábban végeztünk, és egyszerűen a megfelelő helyre kell helyeznünk a tetteink között. Az XState nem szünteti meg a kódírás szükségességét; csak egy strukturált, világos helyet biztosít ennek elhelyezéséhez.

{ actions: { opened: assign({ open: true }), rendered: assign((context, evt) => { const { node } = evt; const dimensions = getResultsListDimensions(node); itemsHeightObserver.observe(node); opacitySpring.set(1, { hard: true }); Object.assign(slideInSpring, SLIDE_OPEN); slideInSpring.update(prev => ({ ...prev, width: dimensions.width }), { hard: true }); slideInSpring.set(dimensions, { hard: false }); return { ...context, node }; }), close() { opacitySpring.set(0); Object.assign(slideInSpring, SLIDE_CLOSE); slideInSpring .update(prev => ({ ...prev, height: 0 })) .then(() => { stateMachineService.send("CLOSED"); }); }, resize(context) { opacitySpring.set(1); slideInSpring.set(getResultsListDimensions(context.node)); }, closed: assign(() => { itemsHeightObserver.unobserve(resultsList); return { open: false, node: null }; }) }
}

Limlom

Az animációs állapotunk az állapotgépünkben van, de hogyan kapjuk meg ki? Szükségünk van a open állapotot az eredménylista megjelenítésének vezérléséhez, és bár ebben a demóban nem használjuk, ennek az automatikus javaslattétel widgetnek a valódi verziójához szüksége van az eredménylista DOM-csomópontjára, például az aktuálisan kiemelt elem nézetbe görgetéséhez.

Kiderül a miénk stateMachineService egy subscribe módszer, amely állapotváltozás esetén aktiválódik. Az átadott visszahívás a gép aktuális állapotával kerül meghívásra, amely magában foglalja a context tárgy. De a Svelte-nek van egy különleges trükkje: a reaktív szintaxisa $: nem csak a komponensváltozókkal és a Svelte tárolókkal működik; minden olyan tárggyal is működik, amelynek a subscribe módszer. Ez azt jelenti, hogy egy ilyen egyszerű dologgal szinkronizálhatunk állapotgépünkkel:

$: ({ open, node: resultsList } = $stateMachineService.context);

Csak egy rendszeres strukturálás, néhány zárójellel, hogy segítsen a dolgok helyes elemzésében.

Egy gyors megjegyzés itt, mint fejlesztendő terület. Jelenleg van néhány olyan műveletünk, amelyek mellékhatásokat is fejtenek ki, és frissítik az állapotot. Ideális esetben ezeket két cselekvésre kellene felosztanunk, az egyik csak a mellékhatás miatt, a másik pedig a használat assign az új állam számára. De úgy döntöttem, hogy a cikkhez a lehető legegyszerűbben tartom a dolgokat, hogy megkönnyítsem az XState bevezetését, még akkor is, ha néhány dolog nem volt egészen ideális.

Itt a demó

Elválás gondolatok

Remélem, ez a bejegyzés felkeltette az érdeklődést az XState iránt. Úgy találtam, hogy ez egy hihetetlenül hasznos, könnyen használható eszköz az összetett állapotok kezelésére. Kérjük, vegye figyelembe, hogy csak a felszínt karcoltuk meg. A minimális fsm csomagra koncentráltunk, de a teljes XState könyvtár sokkal többre képes, mint amit itt tárgyaltunk, a beágyazott állapotoktól kezdve a Promises első osztályú támogatásáig, és még állapotvizualizációs eszköz is van benne! Arra kérlek, hogy nézd meg.

Boldog kódolást!

Forrás: https://css-tricks.com/coordinating-svelte-animations-with-xstate/

Időbélyeg:

Még több CSS trükkök