Elm – Hello world on the map – Elm naslouchá Javascriptu

V minulém díle jsme z aplikace v Elmu poslali do Javascriptu data, tedy model se souřadnicemi, zoomem a id mapy. V Javascriptu se podle nich zobrazila mapa. Pokud však mapu posuneme nebo změníme přiblížení, původní model definovaný v aplikaci v Elmu se neaktualizuje. V tomto díle to napravíme.

Seriál: Elm – Hello world on the map !!! (4 díly)

  1. Elm – Hello world on the map – úvod 29.8.2016
  2. Elm – Hello world on the map – začínáme 5.9.2016
  3. Elm – Hello world on the map – Elm volá Javascript 12.9.2016
  4. Elm – Hello world on the map – Elm naslouchá Javascriptu 19.9.2016

Elm naslouchá Javascriptu

V principu potřebujeme předat událost, která vznikne v javascriptovém mapovém api do Elmu. Postup je stejný pro jakékoliv události, kliknutí, posun mapy, změna mapové vrstvy…

My se nyní zaměříme na posun mapy a změnu zoomu.

Začneme na straně Elmu. Definujeme si nový port.

port onRedraw : (Model -> msg) -> Sub msg

Můžeme si to zkusit zkompilovat (pokud to za nás nedělá automaticky nějaký buildovací nástroj), abychom se ujistili, že v tomto kroku se nestala žádná chyba.

Všimněte si, že port onRedraw má jinou definici než port createSMap. Bude nám sloužit pro naslouchání z javascriptu a k tomu použijeme právě subscriptions.

Nejprve upravíme definici typu zpráv, Msg.

type Msg
    =  RedrawMap Model
    | Noop

Zpráva je výčtový typ, kromě hodnoty Noop jsme si definovali zprávu označenou tagem RedrawMap, která bude mít jako svou hodnotu model. Tag Noop už vlastně ani nepotřebujeme, ale nechal jsem ho tam.

Kompilace nyní selže.

$ elm make Main.elm --output=elm-main.js

screen-elm-compiler-04-01

Opět nám kompilátor skvěle poradil. Všechny zprávy, které jsme jednou definovali, musí umět zpracovat funkce update. Nelze na nic zapomenout. Takže to napravíme.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        Noop ->
            (model, Cmd.none)
        RedrawMap model ->
                ( model, Cmd.none )

Pokud funkce update obdrží zprávu typu RedrawMap, aktualizujeme model (nový model získáme v té zprávě).

Nyní kompilace skončí bez chyby.

Aktualizovat model tedy už zřejmě umíme, ale nejsme si tím vůbec jisti. Jdeme na to totiž od konce a pořád neumíme ty změny do Elmu poslat.

Potřebujeme zapnout subscriptions.

-- SUBSCRIPTIONS

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
            [
              onRedraw RedrawMap
              -- zde bude další subscriptions
              -- a zde třeba ještě další             
            ]

Použití Sub.batch je prozatím nadbytečné, ale předpokládám, že časem budeme chtít naslouchat více signálům (událostem). Prozatím by stačil i takovýto zápis

-- SUBSCRIPTIONS

subscriptions : Model -> Sub Msg
subscriptions model = onRedraw RedrawMap

Nyní máme v naší aplikaci definovány dva porty.

  1. createSMap používáme pro předávání dat z Elmu do Javascriptu pomocí commands.
  2. onRedraw používáme pro předávání aktuálních dat v opačném směru, z Javascriptu do Elmu, pomocí subscriptions

Portů samozřejmě můžeme vytvořit kolik chceme, nám postačí prozatím pro každý směr jeden.

Object, který v Javascriptu získáme pomocí const elm = Elm.Main.fullscreen() nyní obsahuje oba porty.

Object {
    ports: {
        createSMap: {
            subscribe: function subscribe(callback),
            unsubscribe: function unsubscribe(callback)
        },
        onRedraw: {
            send: function send(incomingValue)
        }
    }
}

Výborně. Porty mají odlišné „API“, dle svého účelu.

Javascript volá Elm

Včíl zkusíme z Javascriptu zavolat funkci (port) onRedraw.

V API Seznamu se události nazývají signály. Použijeme signál map-redraw – Obecná změna stavu mapy (střed, zoom, natočení), který posílá mapa. Signálu se přiřadí funkce zpětného volání.

const signals = mapa.getSignals();
signals.addListener(window, "map-redraw", callback);

Callback funkci si nazveme pro přehlednost stejně jako port, který v ní zavoláme, tedy onRedraw.

/*
nastavíme zpětné volání pro signál "map-redraw"
*/
const onRedraw = (e) => { 
    // získáme data z události
    const mapa = e.target

    const center = mapa.getCenter()

    // pošleme data do Elm aplikace
    elm.ports.onRedraw.send(
        {
            center: [center.x, center.y],
            zoom: mapa.getZoom(),
            id: karta.id
        }

    );
}

Voláním elm.ports.onRedraw.send pošleme do Elmu nový model. Ten se pak ve funkci update zpracuje, nejčastěji se nějak změní vnitřní stav Elm aplikace a dle toho se vzápětí překreslí view (používá se virtual DOM, takže se překreslí jen to, co se skutečně změnilo). Aplikace nyní zobrazuje aktuální polohu středu mapy a aktuální přiblížení (zoom). Výborně! (Až na ta desetinná čísla nenaformátovaných souřadnic. To můžeme pojmout jako domácí úkol.)

screen-04-02

A kdo mi zabrání poslat z Javascriptu místo objektu reprezentujicího model něco úplně jiného? Tak to vyzkoušejte! Stejně tak si můžeme říci, že úplně zbytečně předáváme v objektu id mapy, to se přece nemění. Tak schválně:

// pošleme do Elm apliakce maličko jiná data, než jsme si v Elmu definovali
    elm.ports.onRedraw.send(
        {
            center: [center.x, center.y],
            zoom: mapa.getZoom()
            // tohle tam prostě nedáme
            // id: karta.id
        }

    );

A se zlou se potážeme. Kompilátor nám sice nepomůže, protože jsme v Javascriptu a tady si můžeme dělat co chceme, ale aplikace přestane fungovat, a v javascriptové konzoli se dozvíme proč.

elm-main.js:3564 Uncaught Error: Trying to send an unexpected type of value through port `onRedraw`:
Expecting an object with a field named `id` but instead got: {"center":[17.533419555664096,48.821199361819744],"zoom":14}

Co k tomu dodat? Lépe už snad ani výhody staticky typovaného jazyka do Javascriptu implementovat nelze. Jen připomínám, že je to chyba na straně externího Javascriptu. Aplikaci v Elmu to nijak neovlivnilo. Z pohledu aplikace v Elmu se stalo pouze to, že jí nikdo neposlal korektní data. A proto nemá co zpracovávat a na co reagovat. Až si to externí Javascript dá do pořádku, Elm jeho požadavek korektně vyřídí.

Celá javascriptová část této apliakce může vypadat takto. Zde jsem ve funkci onRedraw udělal odesílání nového modelu maličko odlišně. Nevytvářím nový model, ale měním a odesílám objekt získaný při vytváření mapy. Je to jen implementační detail.


/*
načteme api pro mapy.cz
*/
Loader.load()

/*
Když to nebylo obaleno funkcí window.onload,
tak
Uncaught ReferenceError: SMap is not defined
*/

window.onload = function () {

    /*
    spustíme Elm
    */
    const elm = Elm.Main.fullscreen()

    /*
    mapu vytvoříme ve funkci zpětného volání, bude se volat po zavolání createSMap z Elmu
    */
    const createSMap = function (karta)  {
        const center = SMap.Coords.fromWGS84(karta.center[1], karta.center[0])
        const mapa = new SMap(JAK.gel(karta.id), center, karta.zoom)
        mapa.addDefaultLayer(SMap.DEF_BASE).enable()
        mapa.addDefaultControls()

        /*
        nastavíme zpětné volání pro signál "map-redraw"
        */

        const _karta = karta

        const onRedraw = (e) => { 
            
            const mapa = e.target

            const center = mapa.getCenter()

            _karta.center = [center.y, center.x]
            _karta.zoom = mapa.getZoom()

            elm.ports.onRedraw.send(_karta)

        }

        const signals = mapa.getSignals();
        signals.addListener(window, "map-redraw", onRedraw);

    }

    /*
    Nastavíme zpětné volání pro createSMap
    */
    elm.ports.createSMap.subscribe(createSMap);

    
}


V ukázce používám název proměnné karta pro objekt reprezentujicí mapu. Použil jsem to kdysi v jedné své malé knihovničce a ponechal jsem to tak i pro účely tohoto článku, byť to může být poněkud matoucí. Anglické slovíčko map znamená jednak mapa, jednak se používá pro hojně používanou funkci vyššího řádu, bez které se žádný funkcionální jazyk snad ani neobejde. A to se pak skutečně neskutečně motá. V určité fázi zoufalství jsem po vzoru řeckého χάρτης, německého die Karte, ruského карта, francouzského le carte, arménského Քարտեզը (K’artezy), či arabského خريطة (kharita) zvolil počeštělý název karta. To se zase pro změnu plete s kartami v mariáši, zdravotní dokumentací a anglickými výrazy Card a Chart, ale jak se říká, lepší než drátem do oka.

Epilog

V prohlížeči potřebujeme Javascript, cokoliv jiného je nám téměř k ničemu, Java Fx a Adobe Flash jsou minulostí, WebAssembly je možná budoucnost, ta však ještě nepřišla. A a až jednou přijde, bude s Javascriptem spolupracovat, ne ho nahrazovat.

V prohlížeči potřebujeme Javascript, ale to neznamená, že ten Javascript musíme i psát. Ty Internety jsou dnes plné různých projektů, které se snaží psát kód pro prohlížeče jinak, lépe a radostněji, aby jej pak do toho Javascriptu nějakým způsobem převedly.

Elm je dle mého skromného názoru jedním z nejlépe navržených způsobů, jak psát webové aplikace jinak. Oproti Javascriptu přichází s radikálně odlišnou syntaxí nového jazyka a nesmlouvavě se drží čistě funkcionálního paradigmatu. Po překonání těchto překážek však získáte nástroj, který umožní psát webové aplikace úsporně, bezpečně a přehledně.

Tato čtyřdílná sága neměla být jedním z mnoha a mnoha tutoriálů. Mým cílem, můj milý čtenáři, bylo přinutit tě k zamyšlení. Vlastně to ani nebylo třeba, to on, sám velký Elm nás nutí zamýšlet se nad našimi bídnými kódy a ukazuje nám snažší a štastnější cestu. Já jen chtěl ukázat, jak spolupracuje nový a dokonalý Elm se starým a nedokonalým Javascriptem. Za důležité považuji tyto poznatky:

  1. Elm je nový, samostatný jazyk, který můžeme pro běžné účely považovat za jazyk zcela nezávislý na Javascriptu. Že se náš kód v Elmu nakonec musí převést na Javascript, by nás vůbec nemuselo zajímat. Stejně jako se běžný Java vývojář nezajímá o implementační detaily byte kódu.
  2. Pokud přesto není zbytí, můžeme propojit aplikaci v Elmu s externím Javascriptem. Způsob, jakým se to děje, připomíná volání cizí služby, nebo spuštění cizí aplikace. Svět Elmu a Javascriptu zůstávají co možná nejvíce izolované. Je to zcela jiný princip, než když v Kotlinu či Clojure importujeme javovské knihovny, nebo když v Pythonu používáme knihovny jazyka C/C++. Chyba v externím Javascriptu se do aplikace v Elmu nepřenese, navíc nás ještě Elm upozorní, když se mu pokusíme z externího kódu poslat nekorektní data.
  3. Elm komunikuje s externím Javascriptem pomocí portů a využívá commands a subscriptions. Porty jsou v Elmu přímo kvůli komunikaci s externím Javascriptem. Commands a Subscriptions jsou ale obecnější součásti Elm Architecture.

Subscriptions obecně slouží pro naslouchání vnějších vstupů, což mohou být události myši a stisku klávesnice, události browseru jako změna velikosti okna, požadavek na změnu url apod. Stejně tak se subscriptions používají pro komunikaci pomocí Websocketu.

Commands slouží obecně pro spuštění vedlejších efektů, což je kromě volání externího Javascriptu třeba i odeslání http požadavku, uložení dat do local storage či vygenerování náhodného čísla.

Vidíme, že jeden mechanismus má celou řadu použití. V tom se skrývá síla Elmu. Budeme-li chtít inicializovat model na základě dat získaných přes nějaké http API, a později aplikaci přepracujeme, aby se model inicializoval z data uložených v local storage, bude stačit jen malá úprava kódu.

Jak jsem již naznačil, v podstatě by bylo dobré, kdybyste vůbec nepotřebovali a nepoužívali volání externího Javascriptu, jak jsem obsáhle popsal ve svém pojednání. Vždy je lepší si vystačit s Elmem. Pokud se vám mé články líbily, mohu slíbit ještě pokračování.

Doposud jsme se bavili o tom, jak si běžný vývojář může odskočit z Elmu do Javascriptu. Ale samotný Elm samozřejmě funguje jinak. Kód, který kompilátor vytvoří, přitom není generovaný v kompilátoru, je více méně poskládaný ze speciálních, takzvaných nativních modulů. Dokumentace k tomu vlastně žádná není, to, co vrátí vyhledávač na klíčové slovo ‚elm native modules‘ je zastaralé. Platí z toho snad jen ono důrazné varování před živelným vylepšováním Elmu. K tomu nikoho návadět nebudu, ale podívat se pod pokličku, včetně krátké procházky haskellovským kódem kompilátoru, může být poučné a zajimavé.

Jsem programátor na volné noze. Věnuji se především zpracování a prezentaci dat. Zajímám se o programovací jazyky. Začínal jsem s jazykem C/C++ a PHP pro web. Pak jsem se nadchnul pro Python. Když přišel Node.js, začal jsem věnovat více své pozornosti JavaScriptu. Nyní mne zajímají především jazyky Rust a Elm. Krom toho opravuji s přáteli zchátralý dětský tábor v Karpatech (nejen) z hlíny a slámy. A dělám spoustu dalších věcí, kvůli kterým mi na programování nezbývá čas.

Komentáře: 1

Přehled komentářů

JanVoracek JavaScript
Zdroj: https://www.zdrojak.cz/?p=18878