IMA.js – Detailní pohled na komponenty, eventy a extensions

Publikováno: 22.9.2020

V předchozím díle jsme si na naší aplikaci s předpovědí počasí ukázali, jak vykreslit data pomocí Views a rozdělili jsme si HomeView do podrobnějších komponent. V tomto díle uděláme naši aplikaci pro uživatele zajímavější a přidáme možnost jak zadávat lokalitu.

Celý článek

Před přečtením tohoto dílu si prosím stáhněte zdrojové kódy aplikace z GitHubu. V textu tohoto dílu nebudeme pracovat se všemi potřebnými soubory pro zprovoznění aplikace, ale projdeme pouze ty nejdůležitější pro její fungování.

Na konci tohoto dílu bude naše aplikace vypadat tak jako na obrázku.

Animovaná ukázka aplikace

Vyhledávání

V současném stavu je možné změnit lokalitu pro předpověď počasí pouze pomocí parametru v URL. Abychom práci s aplikací uživateli zpříjemnili, vytvoříme v hlavičce stránky komponentu <SearchBar /> pro vyhledávání měst a obcí s nápovědou.

Náhled obrazovky

SearchBar

Pro napovídání měst a obcí (Suggest) si musíme vytvořit proxy a modely, stejně jako pro forecast a geocoder. Tento postup jsme si ukázali v prvním díle tohoto seriálu. Kompletní podobu modelu pro Suggest najdete v ukázkové aplikaci. Budeme opět využívat služby Mapy.cz stejně jako v případně geocoder.

// app/environment.js
prod: {
    // ...
    SuggestProxy: {
        path: '/suggest',
        server: 'https://mapy.cz',
        options: {
            proxyReqPathResolver: function(req) {
                const queryString = req.url.split('?')[1];
                return '/suggest' + (queryString ? '?' + queryString : '');
            }
        }
    }
}
// app/config/settings.js

prod: {
  // ...
  App: {
    api: config.$Protocol + '//' + config.$Host + '/api',
    geocoder: config.$Protocol + '//' + config.$Host + '/geo',
+   suggest: config.$Protocol + '//' + config.$Host + '/suggest',
  }
}

SearchBar extension

Abychom mohli načítat data a reagovat na změnu lokality, vytvoříme si tzn. Extension – doplněk ke klasickým Controllerům. Extension se vždy páruje ke specifické komponentě, a ta může být použita na více stránkách nebo nemusí být použita vůbec, proto nemůže být logika extension obsažena v controlleru. Zároveň extension slouží k tomu, abychom nezahlcovali komponentu logikou načítání dat.

// app/component/searchBar/SearchBarExtension.js

import { AbstractExtension, Router } from '@ima/core';
import SuggestService from 'app/model/suggest/SuggestService';

export default class SearchBarExtension extends AbstractExtension {
  static get $dependencies() {
    return [Router, SuggestService];
  }

  constructor(router, suggestService) {
    super();

    this._router = router;
    this._suggestService = suggestService;
  }

  load() {
    // vrátíme defaultní hodnoty jelikož samotné načítání se bude dít až uživatel zadá nějaký text do vyhledávání.
    return {
      suggestItems: [],
      suggestItemsLoading: false
    };
  }

Zde jsme si naimportovali SuggestService, pomocí které budeme načítat data pro nápovědu a Router, který použijeme pro přesměrování na novou adresu se zvolenou lokalitou.

V metodě load můžeme vidět, jakými klíči bude tato extension přispívat do page-state. V tomto případě jsou hodnoty pouze výchozí, protože samotné hledání a napovídaní se bude dít až po zadání textu. load metoda v extension se chová stejně jako v Controlleru a je detailně popsána v dokumentaci.

  • suggestItems bude obsahovat list SuggestEntity, který se vrátí na dotaz po uživatelem zadaném textu.
  • suggestItemsLoading bude označovat, zda se zrovna načítají data pro nápovědu.

Do SearchBarExtension ještě přidáme 2 metody, které budou reagovat na události z SearchBar komponenty. Povšimněte si speciálních názvů metod, které jsou prefixovány on a poté následuje Upper-CamelCase název události.

async onSuggestItemsLoad({ inputValue }) {
  this.setState({ suggestItemsLoading: true });

  const suggestItems = await this._suggestService.getList(inputValue);
  this.setState({ suggestItems, suggestItemsLoading: false });
}

onSuggestItemSelect({ title, lat, lon }) {
  const newPlaceLink = this._router.link('home', { location: title, lat, lon });

  this._router.redirect(newPlaceLink);
}

SearchBar HOC – High Order Component

Když nám extension načte data, jsou umístěna do page state a předaná do View (v našem případě HomeView.jsx). V ideálním světě bychom si tato data rozdělili do pod-komponent a vše by fungovalo krásně. Co když ale máme hluboko zanořenou komponentu, a ta potřebuje mít přístup k datům z page state, o kterých její nadřazená komponenta neví? V takovém případě se dostáváme k problému známému jako „prop-drilling“.

Pro vyhnutí se „prop-drilling“ použijeme balíček @ima/plugin-select. O nastavení a detailnějším použití tohoto balíčku se dočtete v jeho README. Nás bude zajímat pouze jeho defaultní funkcionalita.

// app/component/searchBar/SearchBar.jsx
import select from '@ima/plugin-select';

class SearchBar extends AbstractComponent {
  // ...
}

const resourcesSelector = state => {
  return {
    key: state.location ? state.location.title : null,
    currentLocation: state.location ? state.location.title : null,
    suggestItems: state.suggestItems
  };
};

export default select(resourcesSelector)(SearchBar);

SearchBar komponenta

Některé metody této komponenty nebudeme vysvětlovat do hloubky, protože jejich implementace není podstatná pro vysvětlení základní funkčnosti. Kompletní podobu komponenty najdete v ukázkové aplikaci.

Tato komponenta má 2 hlavní úkoly:

  1. vykreslit data, která dostala z HOC a
  2. obsloužit události okolo inputu.

Hlavní událostí je onChange, na kterou reagujeme tak, že změníme interní state inputValue a pokud je text delší než 3 znaky vyvoláme událost suggestItemsLoad. Načasování této události je nastaveno tak, abychom ji nevyvolali pro každé napsané písmenko. Místo prostého setTimeout je možné využít jakékoliv metody třetí strany (např. lodash.debounce)

onSearchType(event) {
  const inputValue = event.target.value;
  clearTimeout(this._searchTimeout);

  if (inputValue.length < 3) {
    this.setState({ inputValue, suggestActive: false });
    return;
  } else {
    this.setState({ inputValue });
  }

  this._searchTimeout = setTimeout(() => {
    this.setState({ suggestActive: true, suggestLoading: true });
    this.fire('suggestItemsLoad', { inputValue: this.state.inputValue });
  }, SEARCH_DEBOUNCE_TIME);
}

Poté, co událost suggestItemsLoad odchytí SearchBarExtension, načte potřebná data a SearchBar komponenta je následně vyrenderuje. Další událost nastane, když uživatel klikne na jednu ze zobrazených nápověd. V tomto případě pouze předáme data o lokalitě do události suggestItemSelect a SearchBarExtension se postará o přesměrování uživatele.

onSuggestItemClick(event, item) {
  event.preventDefault();

  const { title, lat, lon } = item;
  this.fire('suggestItemSelect', { title, lat, lon });
}

Závěr

Pomocí komponenty a extension SearchBar se nám aplikaci povedlo oživit a díky ní jsme se naučili jak spolupracují komponenty a extensions. V naší aplikaci jsme komponentu SearchBar umístili přímo do HomeView, tudíž může být použití @ima/plugin-select rozporuplné, protože si můžeme data z page state předat napřímo. Vemte ale na vědomí, že takto je celá funkcionalita SearchBar úplně oddělená a je možné ji použít kdekoliv v aplikaci aniž bychom se museli starat jaké props máme komponentě předat.

Za poslední 3 díly tohoto seriálu jsme vytvořili obstojnou aplikaci, která zobrazuje předpověď počasí pro uživatelem zadané místo. Zdaleka jsme ale tímto seriálem nemohli pokrýt všechnu funkcionalitu IMA.js frameworku, proto bychom vás rádi odkázali na Dokumentaci a také přehled API, kde můžete dohledat vše co potřebujete.

V dalším díle seriálu se zaměříme na strojové testování naší aplikace pomocí integračních a unit testů.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace