Przykładowa aplikacja - panel administratora

W tym rozdziale będziemy tworzyć krok po kroku przykładową aplikację z panelem administratora. Zakładamy, że mamy zainstalowany program yarn oraz TypeScript.

Startujemy

Tworzymy katalog / folder (np. myskeleton) z podkatalogami:

  • src - źródła aplikacji
  • build - zbudowana strona
  • public - pliki html do osadzenia aplikacji
  • api - katalog w którym będziemy projektować API

Następnie w tym katalogu przy pomocy wcześniej zzainstalowanego programu yarn inicjujemy aplikację. Jako jej punkt startowy wskazujemy plik index.tsx (zwróć uwagę na rozszerzenie nazwy .tsx - wskazujące na TypeScript).

Instalujemy też pakiet react-scripts - który zawiera skrypty do uruchamiania i budowania aplikacji.

W systemie Linux wykona to skrypt:

#!/bin/bash

mkdir myskeleton
cd myskeleton
mkdir src
mkdir build
mkdir public
mkdir api

echo "answer for question -  entry point: index.tsx"
yarn init

yarn add react react-dom redux react-redux redux-logger redux-thunk redux-compact \
         typescript @types/react @types/react-dom @types/react-redux \
          @material-ui/core axios
yarn add --dev react-app-rewired react-app-rewire-alias \
         react-scripts swagger-axios-codegen
exit 0

Tworzymy niezbędne pliki:

tsconfig.json - wskazuje, że mamy do czynienia z aplikacją TypeScript

{
}

public/index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Start</title>
    </head>
    <body>
        <noscript>
            You need to enable JavaScript to run this app.
        </noscript>
        <div id="root"></div>
    </body>
</html>

src/index.tsx

import * as React from "react";
import * as ReactDOM from "react-dom";

ReactDOM.render(
    <div>Hello World
    </div>,
    document.getElementById("root")
);

Do pliku package.json dodajemy opis uruchamiania skryptów:

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "clean": "rm -rf ./api/*.ts",
    "build:api": "npm run clean && node ./api/codegen.js"
 },

Cztery pierwsze definicje uruchamiają react-scripts z odpowiednimi parametrami. Kolejne dwie posłużą nam do wygenerowania API z użyciem swagger-axios-codegen.

W katalogu api przygotowujemy skrypt generatora codegen.js:

const { codegen } = require('swagger-axios-codegen')

codegen({
  methodNameMode: 'operationId',
  source: require('./openapi.json'),
  outputDir: './api'
})

Możemy już przetestować naszą aplikację:

yarn start

Przygotowujemy API

Do zdefiniowania API posłuży nam standard OpenAPI. Możemy plik z definicją zbudować w portalu https://editor.swagger.io/. Można także skorzystać z jakiegoś pakietu oprogramowania, który pozwoli równocześnie stworzyć serwer odpowiadający na zapytania API.

My skorzystamy z tej drugiej możliwości (oprogramowanie dostępne w repozytorium github) - używając do tego pakietu flask-restx.

Oto przykładowy moduł logowania:

# coding: utf-8

from flask_restx import fields
from flask_restx import Namespace, Resource
from flask_restx import reqparse

api = Namespace('user', description='Login')

Session = api.model('Session', {
    'session': fields.String,
    'message': fields.String,
    'retcode': fields.Integer,
})

@api.route('/login',methods=['POST',])
@api.response(404, "User not found")
class Login(Resource):

    @api.marshal_with(Session,  as_list=False)
    @api.param('username', 'User name')
    @api.param('password','Password')
    def post(self):
      parser = reqparse.RequestParser()
      parser.add_argument('username', required=True, location='json')
      parser.add_argument('password', required=True, location='json')
      args = parser.parse_args()
      (username, password) = ( args['username'], args['password'] )
      try:
        if (username==password) and (username=='demo'):
          return {
           'session': 999,
           'message': 'OK',
           'retcode': 200
          }
        else:
          api.abort(404)
      except Exception as e:
        return {
           'session': -1,
           'message': 'Errror! {0}'.format(e),
           'retcode': 500
        }

Mamy trzy główne sposoby przesyłania danych protokołem HTTP:

1) W adresie URL (najczęściej stosowane jedynie przy metodzie GET) - rozdzielone znakiem &. Na przykład:

http://127.0.0.1:5000/user/login?username=demo&password=demo

2) W nagłówku zapytania. Możemy to testować programem cur - podając parametry z przełącznikiem H (można użyć go wielokrotnie w jednym wierszu poleceń):

curl -X POST "http://127.0.0.1:5000/user/login" -H "accept: application/json" \
            -H "username: demo" -H "password: demo"

Fragment nagłówka wysyłanego w tym poleceniu:

POST /user/login HTTP/1.1
accept: application/json
password: demo
username: demo
....

3) W postaci danych zawartych w ciele (body) zapytania. Tu dodatkowo mamy kilka możliwości formatowania tych danych. Najczęściej jest to format pól formularza:

curl -X POST "https://editor.swagger.io/user/login" \
      -H "accept: application/json" -H "Content-Type: application/json" \
      -d "username=demo&password=demo"

W naszym przykładzie założyliśmy, że dane są przesyłane w formacie JSON. Stąd konieczność użycia parsera (parser) w kodzie na serwerze (Python). Testowe zapytanie curl ma format:

curl -X POST "http://127.0.0.1:5000/user/login" -H "accept: application/json" \
  -H "Content-Type: application/json"\
  -d "{ \"username\": \"demo\", \"password\": \"demo\" }"

Pod adresem http://127.0.0.1:5000/swagger.json uzyskujemy zapis API w postaci JSON (można go edytować online: https://editor.swagger.io/). Otrzymany fragment API:

    "paths": {
        "/user/login": {
            "post": {
                "tags": [
                    "user"
                ], 
                "responses": {
                    "200": {
                        "description": "Success", 
                        "schema": {
                            "$ref": "#/definitions/Session"
                        }
                    }, 
                    "404": {
                        "description": "User not found"
                    }
                }, 
                "parameters": [
                    {
                        "type": "string", 
                        "description": "User name", 
                        "name": "username", 
                        "in": "query"
                    }, 
                    {
                        "type": "string", 
                        "description": "Password", 
                        "name": "password", 
                        "in": "query"
                    }, 
                    {
                        "description": "An optional fields mask", 
                        "format": "mask", 
                        "type": "string", 
                        "name": "X-Fields", 
                        "in": "header"
                    }
                ], 
                "operationId": "post_login"
            }
        }
    }, 
    "definitions": {
        "Session": {
            "type": "object", 
            "properties": {
                "message": {
                    "type": "string"
                }, 
                "session": {
                    "type": "string"
                }, 
                "retcode": {
                    "type": "integer"
                }
            }
        }
    },

Plik zapisujemy w api/openapi.jsoni wykonujemy generowanie modułu Axios:

yarn run build:api

W wygenerowanym pliku dodajemy odpowiedni adres serwera. Na przykład:

const serverURL = 'http://127.0.0.1:5000/'

....
let url = serverURL+'/user/login';

Niestety generator w obecnej wersji ma błąd i nie działa poprawnie dla wysyłania danych jako JSON. Musimy wykonać drobną poprawkę - jeśli chcemy wysyłać dane jako JSON (jak to założono wyżej):

kod wygenerowany:
      configs.params = { username: params['username'], 
                           password: params['password'] };
      let data = null;
kod poprawny:
      configs.params = {};
      let data = JSON.stringify({ username: params['username'], 
                           password: params['password'] });

Powyższe logowanie jest bardzo proste - nie wykorzystuje w ogóle ciasteczek (Cookie) - zakładając, że sesja trwa od zalogowania do zamknięcia aplikacji (np. wskutek odświeżenia przeglądarki). W przeciwieństwie do zwykłych stron internetowych, taka koncepcja w aplikacji webowej ma sens.

Na koniec tworzymy komponent logowania. Dla uproszczenia załóżmy na początek, że gdy użytkownik jest zalogowany - wyświetla się numer sesji, a w przeciwnym przypadku: formularz logowania.

Warto się chwilę zatrzymać na tym module. Poniżej omówimy jego strukturę. Dlatego przytaczamy go w całości.

import * as React from "react"
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';

import { Session, UserService } from '../api'


type ParState = {
  ok: boolean,
  message: string,
  session?: Session
}
type ParProps = {
}

export default class LoginForm extends React.Component<ParProps, ParState>  {

  v_username: string | any;
  v_password: string | any;

  change_username = (e: React.FormEvent) => {
    let target = e.target as HTMLInputElement;
    this.v_username = target.value;
  }
  change_password = (e: React.FormEvent) => {
    let target = e.target as HTMLInputElement;
    this.v_password = target.value;
  }

  constructor(props: any) {
    super(props, {});
    this.state = {
      ok: false,
      message: ''
    };
  }

  async apiLogin(p_username: string, p_password: string) {
    UserService.postLogin({username:p_username,password:p_password},{} ).then(
      (ret: Session) => {
        this.setState({
          ...this.state,
          session: ret
        })
      }
    ).catch((err: any) => {
      alert('API Error: ' + p_username+ ' == '+
               p_password+' == '+JSON.stringify(err));
    }
    );
  }

  render = () => {

    let btnSetPasswordClick = () => {
      this.apiLogin(this.v_username, this.v_password)
    }

    if (this.state.session) {
      return (<div> Session: {this.state.session.session} </div>)
    }
    return (
      <form id="passwordext-form" name="passwordext-form"
        method="post" action="">
        <TextField
          onChange={(x: React.FormEvent) => this.change_username(x)}
          label="username"
        />
        <br />
        <TextField
          onChange={(e: React.FormEvent) => this.change_password(e)}
          label="password"
          type="password" />
        <br />
        <span style={{ color: 'red' }}>
          {this.state.message}
        </span>
        <br />
        <Button variant="contained" color="primary"
          onClick={btnSetPasswordClick}>
          Login
        </Button>
      </form>
    )

  }
}
  • Na początek importy: Ponieważ chcemy skorzystać z biblioteki material-ui - dołączamy odpowiednie dla Typescript komponenty. Z naszego API importujemy zdefiniowany model Session, oraz komponent komunikacji UserServ.
  • W definicji komponentu podajemy typ parametrów (params) oraz stanu (state) . Wykorzystujemy do tego typy generyczne.
  • Użycie pól obiektowych v_user i v_password jest związane z użyciem formularza z wywoływaniem funkcji rejestrujących każdą zmianę pól (onChange). Inne rozwiązania można znaleźć w porównaniu: https://www.merixstudio.com/blog/react-forms-comparison/

  • Po kliknięciu w przycisk wywoływana jest funkcja btnSetPasswordClick która z kolei powoduje asynchroniczne wywołanie API. Po zakończonej sukcesem transmisji, zostaje wykonany fragment zaczynajacy się od definicji:(ret: Session) => {

  • Wywołanie zmiany stanu (this.setState) powoduje ponowne renderowanie komponentu. Ponieważ jest to wykonane po ustawieniu elementu state.session - formularz nie jest na nowo wyświetlany.

Panel administracyjny

Do budowy panelu skorzystamy z projektu https://github.com/anhtrac43nd/material-dashboard-react-ts

Zawiera on wszystkie potrzebne nam elementy:

1) Style do "malowania" elementów aplikacji - zgromadzone w katalogu assets/jss

2) Wzorzec (layout) strony (layouts/Dashboard)

3) Routing definiowany w postaci listy (routes)

Po przekopiowaniu powyższych elementów rozpoczynamy od zmiany pliku index.tsx, z wykorzystaniem routingu:

ReactDOM.render(
  <Router history={hist}>
    <Switch>
      {indexRoutes.map((prop, key) => {
        return <Route path={prop.path} component={prop.component} key={key} />;
      })}
    </Switch>
  </Router>,
  document.getElementById('root'),
);

Widzimy tu generowanie manu (routingu) na podstawie zdefiniowanej listy. Wykorzystywana jest do tego funkcja map().

Uruchomienie:

** Dodajmy do tsconfig.json zapis umożliwający podawanie ścieżek względem katalogu src (taką zasadę zastosowano w wykorzystanym projekcie):

 "compilerOptions": {
    "baseUrl": "src",
 ...

**Instalujemy material-ui i inne potrzebne biblioteki

yarn add @material-ui/icons @material-ui/core classnames history perfect-scrollbar \
         react-popper @popperjs/core react-router-dom redux-logger
yarn add --dev @types/classnames @types/history @types/react-router \
         @types/react-router-dom @types/redux-logger

Style

Wykorzystując bibliotekę material-ui - przyjmujemy charakterystyczne dla niej konwencje obsługi styli.

1) Wygląd komponentu zależy od jego klasy (className). Nazwa klasy jest generowana przez obiekt / funkcję classes, dostarczany w postaci jednej z własności (props). Typowy przykład:

  render() {
    const { classes, ...rest } = this.props;

    return (
      <div className={classes.content}>
 ...

Nazwa content w powyższym przykładzie, to jeden z elementów stylu.

Aby to było możliwe - musimy zadeklarować własność classes. Najproście w postaci pola dowolnego typu:

interface Props {
  classes? : any
}

interface State { }

class App extends React.Component<Props, State> {

Możemy oczywiście zdefiniować strukturę oczekiwanych styli. Na przykład:

interface Props {
  classes: {
    content: string;
    container: string;
  }
}

Wtedy dostarczany styl musi mieć taką właśnie strukturę. Powinnuśmy do jego tworzenia użyć funkcji createStyles (zob. https://material-ui.com/styles/api/. Na przykład:

const appStyle = createStyles(
{   
  content: {
    marginTop: '70px',
    padding: '30px 15px',
    minHeight: 'calc(100% - 123px)',
  },
  container {
    marginTop: '70px',
  },
})

2) Style są zazwyczaj do własności wstrzykiwane funkcją withStyle. Przykład:

export default withStyles(appStyle)(App);

3) Argumentem withStyles (w powyższym przykładzie appStyle) może być funkcja generująca styl na podstawie tematu (Theme). Więcej informacji na ten temat znajdziesz na stronie opisującej API: https://material-ui.com/styles/api/

W odniesieniu do powyższego przykładu będze to wyglądać następująco:

import { createStyles } from '@material-ui/core';
import { Theme } from "@material-ui/core/styles";

const appStyle = (theme : Theme) => createStyles({
  content: {
    marginTop: '70px',
    padding: '30px 15px',
    minHeight: 'calc(100% - 123px)',
  },
  container {
    marginTop: '70px',
  },
})

Taka konstrukcja umożliwia definiowanie stylu poprzez odwołanie do elementów tematu (Theme).

Ponieważ biblioteka material-ui "żyje" - wiele z przykładów jakie można znaleźć w internecie jest nieaktualnych. Powyższy opis powstał 1 maja 2020 i dotyczy najnowszej wersji wspomnianej biblioteki.

Z powyższych powodów konieczne było zmodyfikowanie styli, jakie odziedziczono po projekcie material-dashboard-react-ts.

Kolorystyka

Powyżej wspomniano, że w definicji stylu można używać danych z tematu (Theme). Jak uzyskać taki temat dla material-ui? Kolorystykę będziemy definiować w katalogu cfg.

Zacząć powinniśmy od zdefiniowania podstawowych jego danych - co można zrobić w jednym z dostępnych edytorów online.

Na stronie: http://mcg.mbitson.com można stworzyć dwie używane w Material palety kolorów. Portal zapamiętuje nasze definicje. Cały temat możemy uzyskać na stronie https://www.materialpalette.com lub z projektu open source: https://in-your-saas.github.io/material-ui-theme-editor). Więcej interesujących stron na ten temat: https://material-ui.com/discover-more/related-projects/

Jednym z najprostszych sposobów stworzenia tematu jest wybór koloró na stronie https://material.io/resources/color a następnie skopiowanie uzyskanego linku na stronę generatora tematu: https://react-theming.github.io/create-mui-theme/

Dla naszego przykładu wybieramy kolory: https://material.io/resources/color/#!/?view.left=0&view.right=0&primary.color=1A237E&secondary.color=7B1FA2

Temat zapisujemy w pliku cfg/theme.json, a jego użycie definiujemy w pliku cfg/theme.tsx.

import themeDetails from './theme.json';
import { createMuiTheme } from '@material-ui/core/styles';

export default  createMuiTheme( themeDetails );

Jeśli użyty generator ne jest w pełni kompatybilny z aktualną wersją biblioteki material-ui - trzeba plik teme.json nieco zmodyfikować (w naszym przykładzie usunięto niekompatybilne definicje).

Użycie tematu definiujemy w pliku index.jsx:

import { MuiThemeProvider  } from '@material-ui/core/styles';
import theme  from './cfg/theme';

ReactDOM.render(
 <MuiThemeProvider theme={theme}>
....
 </MuiThemeProvider>

Możemy przetestować wykonaną pracę - na przykład zmieniając w pliku headerStyle.ts definicję dla klasy appBar:

  appBar: {
    ...
    backgroundColor: theme.palette.primary.dark,
    ...
    color: 'white',.
    ...

Powinniśmy zaobserwować zmieny kolorów górnej belki na stronie.

Wielojęzyczność

Pliki z tekstami wielojęzycznymi zdefiniujemy także w podkatalogu cfg. Wykorzystamy projekt i18next. Można się z nim, zapoznać na stronach:

Instalujemy odpowiednie paczki:

yarn add i18next @types/i18next react-i18next \
    i18next-browser-languagedetector  @types/i18next-browser-languagedetector \
    i18next-xhr-backend

Defiuniujemy pliki językowe (tłumaczymy przykładowy tekst LogoName):

cfg/en.json

{
  "translations": {
    "LogoName": "App Skeleton"
  }
}

cfg/pl.json

{
  "translations": {
    "LogoName": "Szkielet App"
  }

Tłumaczenia definiujemy w obiekcie import i18n z modułu i18next. Poniższy moduł ustawia odpowiednie parametry tłumaczenia (cfg/i18n.ts):

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LngDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-xhr-backend';

import en from './locale/en.json';
import pl from './locale/pl.json';

i18n
  .use(Backend)
  .use(LngDetector)
  .use(initReactI18next) // passes i18n down to react-i18next
  .init({
    resources: {
      pl, en,
    },
    fallbackLng: 'en', // use pl if detected lng is not available
    keySeparator: false, // we do not use keys in form messages.welcome
    nsSeparator: '|',
    saveMissing: true,
    defaultNS: 'translations', // the same as in locales/en.json !!!!! 
    debug: true,
    interpolation: {
      escapeValue: false // react already safes from xss
    },
  });

i18n.changeLanguage('pl')

export default i18n;

Zwróć uwagę na definicję defaultNS - musi być taka jak użyta w plikach językowych (tu 'translations').

Na koniec ustawiamy provider dostarczający tłumaczenia aplikcji. Tradycyjnie robimy to w pliku głównym aplikacji index.tsx.

import i18n from './cfg/i18n'; 
import { I18nextProvider } from 'react-i18next';


ReactDOM.render(
<I18nextProvider i18n={i18n as any}>
   <MuiThemeProvider theme={theme}>
.....

   </MuiThemeProvider>
</I18nextProvider>

Użyć tłumaczenia możemy na dwa sposoby.

1) Sosując komponent Trans.:

import { Trans, WithTranslation } from 'react-i18next';

class App extends React.Component<Props & RouteProps & WithTranslation, State> {

Dodajemy typ generyczny WithTranslation (zwróć uwagę na dużą literę W) do własnosci (props) komponentu. Możemy już stosować komponent Trans.

Wpisanie <Trans>LogoName</Trans> spowoduje wyświetlenie tekstu w odpowiednim języku.

Jeśli tekst ma być własnością komponentu, lub użyty w jakiejś funkcji - stosujemy drugi sposób - z użyciem funkcji t. Funkcję tą możemy wstrzyknąć do komponentu funkcją withTranslation (mała litera w). Oto odpowiednie fragmenty definicji:

import { withTranslation } from 'react-i18next';
...
interface Props {
  classes? : any;
  t : any
}

...
  render() {
    const { classes, t, ...rest } = this.props;

    return (
      <div className={classes.wrapper}>
        <Sidebar
          logoText={t('LogoName')}

export default withStyles(appStyle)(withTranslation()(App))

Parametryzacja

Załóżmy, że w pliku cfg/config.ts zawrzemy najostotniejsze dla programu parametry działania - na przykład wyświetlane języki, czy parametry połączenia z serwerem:

type Config = {
    apiURL: string,
    apiKey: string,
    lang : string[]
};
let CONFIG: Config = {
    apiURL:  'http://127.0.0.1:5000',
    apiKey: '123456',
    lang: ['en','pl']
};

export default CONFIG;

W każdym miejscu systemu, wymagającym parametryzacji - możemy wpisać:

import CONFIG from 'cfg/config'

by używać parametrów z przedrostkiem CONFIG.

Zmiana parametrów wymaga ponownej kompilacji aplikacji. Istnieje jednak sposób aby tego uniknąć. W nagłówku pliku bazowego public/index.html dodamy wywołanie skryptu z parametrami:

<script src="config.js"></script>

Plik ustawia parametry w drzewie DOM - tworząc węzeł window.config:

window.config = {
   apiURL : 'http://127.0.0.1:5000',
   apiKey: '123456',
   lang : ['en','pl']
};

W pliku cfg/config.ts zdefiniujmy tylko ustawienie parametrówpod warunkiem, że plik ten jest dostępny (zob. https://stackoverflow.com/questions/50488121/typescript-add-functions-to-window-with-d-ts-file\:

import { string } from "prop-types";

type Config = {
    apiURL: string,
    apiKey: string,
    lang : string[]
};
let CONFIG: Config = {
    apiURL:  'http://127.0.0.1:5000',
    apiKey: '123456',
    lang: ['en','pl']
};

interface WindowInterface extends Window {
  config: Config;
}

let window2config = () => { // uruchamiane w App : render()
  var config : Config = (window as unknown as WindowInterface).config || {
    // if missing  public/config.js
    apiURL : CONFIG.apiURL,
    apiKey : CONFIG.apiKey,
    lang: CONFIG.lang
  }
  CONFIG.apiURL = config.apiURL;
  CONFIG.apiKey = config.apiKey;
  CONFIG.lang = config.lang;
}

export {window2config}
export default CONFIG;

Dodatkowo w pliku src/window.d.ts deklarujemy:

declare global {
    interface Window {
      config: any;
    }
  }

Wczytanie (np. w pliku App.tsx; w naszej aplikacji w views/Dashboard/Dashboard):

import { window2config } from '../cfg/config';

  render() {
    window2config();
....

Redux - ożywiamy aplikację

Wykorzystamy na poczatek Redux do zapamiętania języka i zalogowanego użytkownika.

Nasza pamięć (storage) będzie miała dwa pola: lang i session. Zdefiniujemy też ustawiające ten stan funkcje reduktora:

import { applyMiddleware, createStore, Store } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import { definition, create, combine, StateOf } from 'redux-compact';

import i18n from '../cfg/i18n'
import CONFIG from 'cfg/config';

interface IStore {
  session? : string;
  lang : string; 
};

const reduxStore = definition<IStore>()
  .setDefault(
    { session : undefined, 
      lang : CONFIG.lang[0]
    })
  .addReducers({
    setSession: (reduxStore: IStore, newSession : string) => (
       {...reduxStore, session : newSession }),
    setLang : (reduxStore: IStore, newLang : string) => 
      {   i18n.changeLanguage(newLang)
          return({...reduxStore, lang: newLang }) 
       }
  });


const appState = combine({
  reduxStore
});

const { Actions, reduce } = create(appState);
export type AppState = StateOf<typeof appState>;

export const store: Store<AppState> = createStore(  reduce,
  applyMiddleware(thunk, logger),
);

export { Actions  };

Definiujemy provider dla dostarczenia storage do aplikacji (plikindex.tsx):

import { Provider } from 'react-redux';
import {store } from './store'

<MuiThemeProvider theme={theme}>
  <Provider store={store}>
   <Router history={hist}>

Do zmiany języka wykorzystamy ikony flag z biblioteki react-flagkit, którą instalujemy przy pomocy yarn add.

Biblioteka ta nie ma zdefiniowanych dla TypeScript struktur (@types/react-flagkit) - więc użycie tego modułu musi być związane z jedo zadeklarowaniem w declarations.d.ts:

declare module 'react-flagkit';

Komponent wyświetlający ikony flag i reagujący na kliknięcie (literką A oznaczony aktualny język):

import React from 'react';
import { IconButton, Badge } from '@material-ui/core';

import { connect } from 'react-redux';
import { AppState, Actions, store } from '../../store'

import { Theme, withStyles } from "@material-ui/core/styles";
import { PropTypes } from "@material-ui/core"
import Flag from 'react-flagkit'
import CONFIG from '../../cfg/config'

interface FlagIconsProps {
  classes? : any,
  redux? : any
}

class FlagIcons extends React.Component<FlagIconsProps, any> {

  public render() {
    const { classes, } = this.props;
    const lang = CONFIG.lang;

    let flgs : any[] = [];
    lang.map( ( c : string) => {
      if(CONFIG.lang.includes(c)){
        let bg='';
        let clr : PropTypes.Color;
        let cc : string;
        if (c=='en') cc='GB'
        else cc=c.toUpperCase()
        if (this.props.redux.lang==c) {bg='A';clr='secondary'}
        else  {bg='';clr= 'default'}
        flgs.push(

        <IconButton color="inherit">
        <Badge badgeContent={bg} color={ clr }>
        <span onClick={ ()=>{ 
          store.dispatch(Actions.reduxStore.setLang( c ));
        }}>
        <Flag country={cc} />
        </span>
        </Badge>
        </IconButton>  
        );
        }
      }
    );

    return (
    <div className={ classes.itnl }>
      <div style={{paddingTop: "12px"}}>
{flgs}
      </div>
    </div>
  )}
}

const mapStateToProps = (state : AppState) => ({
    redux : state.reduxStore
  });

const styles = (theme : Theme) => ({
    itnl: {
      marginLeft : "2em",
      marginRight: '2em'
    }, 
}); 

export default connect(mapStateToProps)(withStyles(styles)(FlagIcons));

Umieszczamy komponent w stopce (Footer) i możemyobserwować zmiany języka.

Logowanie

Komponent redux-compact został wyposażony w plugin, który umożliwia dodawania funkcjonalności.

Wykorzystamy go, by zdemonstrować składanie Storage reduxa z wielu komponentów. Fragmenty zmienionej implementacji użycia Redux'a:

import { setValueReducer  } from 'redux-compact/plugins';

const auth = definition<string | undefined>().
  setDefault(window.localStorage.getItem('mySession') || undefined).
  use(setValueReducer).addActionCreators({
    setSession: function (session: string) {
      window.localStorage.setItem('mySession', session);
      return this.setValue(session);
    },
    resetSession: function () {
      window.localStorage.removeItem('session');
      return this.setValue(undefined)
    }
  });


const appState = combine({
  auth,
  reduxStore
});

Użycie konstrukcji use(setValueReducer).addActionCreators({umozliwia dodawanie akcji (addActionCreators). W naszym przypadku dodajemy dwie funkcje działające na lokalnej pamięci przeglądarki (localStorage). Pamięć ta umożliwia przechowanie zmiennej (tu: mySession) także w przypadku odświeżenia przeglądarki.