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.json
i 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:
- https://github.com/i18next/react-i18next/blob/master/example/react/src/i18n.js
- https://github.com/dosandk/react-ssr-app/
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.