Differences

This shows you the differences between two versions of the page.

Link to this comparison view

pw:laboratoare:06 [2022/04/27 12:18]
alexandru.hogea [Metode Extensie]
pw:laboratoare:06 [2023/05/08 09:55] (current)
ciprian.dobre [Scopul laboratorului]
Line 1: Line 1:
-====== Laboratorul 06: Structura BackendApi ======+====== Laboratorul 06: Gestionarea starii, Redux Toolkit si formulare. ======
  
-===== Scopul Laboratorului ===== 
  
-Acest laborator este o continuare a laboratorului ​precedent si va finaliza structura proiectului. Vom discuta despre cum am structurat partea de Api a proiectului si vom prezenta detalii despre tehnologiile utilizate, atat din acest laborator, cat si din cel precedent.+===== Scopul ​laboratorului ​=====
  
-===== Cuvant inainte =====+In acest laborator vom intra in detaliu legat de gestionarea starii aplicatiei folosind **hook**-uri si **Redux Toolkit**. Motivul pentru care este nevoie de gestiunea starii aplicatiei este ca anumite componente in diferite locatii ale aplicatiei au nevoie sa imparta aceleasi date. O posibilitate pentru a propaga datele la mai multe componente este ca o compenta parinte sa trimita la descendentii sai datele prin proprietati din copil in copil, insa aceasta abordare poate aglomera componentele si duce la cod greu de gestionat. Alternativa cea mai buna este ca datele partajate de diferite componente sa fie puse la dispozitie printr-o stare globala accesibila prin functii speciale numite hooks. De asemnea, vom prezenta in acest laborator si modalitati de a crea formulare pentru a executa mutatii pe backend si cum puteti gestiona starea formularului cu validarea datelor introduse.
  
-Va recomandam sa aveti deschis tutorialul oficial de la microsoft pentru [[https://​docs.microsoft.com/​en-us/​aspnet/​core/​fundamentals/​minimal-apis?​view=aspnetcore-6.0|.NET 6 minimal API]]. ​+===== Gestionarea starii =====
  
-In plus, va recomandam ​sa utilizati canalul ​de Teams "​Discutii Laborator"​ pentru orice intrebare referitoare la laborator. +La laboratoarele precedente a fost prezentata definirea componentelor ca extindere a clasei React.Component si definirea componentelor in mod functional. ​In cele mai multe cazuri daca componenta are nevoie sa de logica mai complexa este de preferat ​sa fie definita ca componenta functionala motivul fiind ca logica complexa a componentei poate fi sparta si chiar extrasa prin intermediul ​de hook-uri.
-===== Organizarea Codului API =====+
  
-Api-ul este al 3-lea proiect creat in cadrul solutiei noastre. Rolul sau este sa expuna rute catre exterior si sa foloseasca elementele de **domeniu** (core) si **infrastructura** pentru a realiza actiuni.+Un **hook**, asa cum a fost prezentat in laboratoarele precedente cu exemplu ​**useState**, este o functie speciala care este apelata de catre framework in mod automat cand variabilele de care depinde se modifica iar iesirile acestei functii daca se modifica vor declansa recalcularea componentei de catre framework.
  
-In general, cand se urmareste arhitectura Clean, ​in proiectul de API trebuie scrise configuratiile de server (printre care si **dependency injection**)middlewares ​si rute.+Componentele functionale pot apela in interior hook-uri dar si hook-urile la randulor lor pot apela alte hook-uripractic dezvoltatorul poate sa-si creeze propriile hook-uri particularizate din altele. Hook-urile ajuta ca logica sa unei componente sa fie separata de definirea UI-ului pentru a degreva componenta de anumite responsabilitati cum este gestionarea starii interne.
  
-{{:pw:​laboratoare:​solutionexplorerapi.png?400|}}+Mai jos este un exemplu de hook particularizat pentru gestionarea starii paginarii preluat din aplicatia [[https://gitlab.com/​mobylabwebprogramming/​reactfrontend|demo]] a nostra.
  
-<note important>Modul in care noi am organizat codul nu reflecta o strategie generalaci doar o parere personala.</note>+<code> 
 +/** 
 + * This is the pagination controller hook that can be used to manage the state for paged entries. 
 + */ 
 +export const usePaginationController = () => { 
 +    const [pagesetPage] = useState(1);​ // Create a state for the current page. 
 +    const [pageSize, setPageSize] = useState(10); ​// Create a state for the current page size. 
 +    const setPagination = useCallback((newPage:​ number, newPageSize:​ number) ={ // Create a callback to set both the current page and page size. 
 +        setPage(newPage);​ 
 +        setPageSize(newPageSize);​ 
 +    }, [setPage, setPageSize]);​
  
 +    return { // Return the state and its mutations.
 +        page,
 +        pageSize,
 +        setPage,
 +        setPageSize,​
 +        setPagination
 +    }
 +}
  
-==== Program.cs ====+</​code>​
  
-Incepand ​cu .NET 6, Program.cs este punctul ​de acces si configurare al unui server. Pana in .NET 6, Program.cs era folosit ca punct de acces si Startup.cs servea rol de fisier de configurare.+Observati ca hook-ul se numeste incepand ​cu **use** ca sa se faca distinctia ​de alte functii ​in codNoi o sa numim hook-urile care contin logica unei componente **controller hook**Mai jos e un exemplu ​de cum se poate folosi hook-ul precedent in altul impreuna cu alte hook-uri.
  
-<​code ​c#> +<​code>​
-using BookLibrary.Api.Authorization;​ +
-using BookLibrary.Api.Infrastructure;​ +
-using BookLibrary.Api.Web;​ +
-using MediatR; +
-using System.Reflection;​+
  
-var builder ​WebApplication.CreateBuilder(args);+/** 
 + * This is controller hook manages the table state including the pagination and data retrieval from the backend. 
 + */ 
 +export const useUserTableController ​() => { 
 +    const { getUsers: { key: queryKey, query }, deleteUser: { key: deleteUserKey,​ mutation: deleteUser } } = useUserApi();​ // Use the API hook. 
 +    const queryClient = useQueryClient(); // Get the query client. 
 +    const { page, pageSize, setPagination } = usePaginationController();​ // Get the pagination state. 
 +    const { data, isError, isLoading } = useQuery([queryKey,​ page, pageSize], () => query({ page, pageSize })); // Retrieve the table page from the backend via the query hook. 
 +    const { mutateAsync:​ deleteMutation } = useMutation([deleteUserKey],​ deleteUser);​ // Use a mutation to remove an entry. 
 +    const remove = useCallback( 
 +        (id: string) => deleteMutation(id).then(() => queryClient.invalidateQueries([queryKey])),​ 
 +        [queryClient,​ deleteMutation,​ queryKey]); // Create the callback to remove an entry.
  
-builder.Services.AddControllers(options ​=> +    const tryReload = useCallback( 
-+        () => queryClient.invalidateQueries([queryKey]), 
-    options.Filters.Add<​ApiResponseExceptionFilter>​(); +        ​[queryClient,​ queryKey]); // Create a callback to try reloading the data for the table via query invalidation.
-});+
  
-builder.Services.AddEndpointsApiExplorer();+    const tableController = useTableController(setPagination,​ data?.response?.pageSize); // Adapt the pagination for the table.
  
-// Add Swagger with Bearer Configuration +    return { // Return the controller state and actions. 
-builder.Services.AddSwaggerWithBearerConfig();+        ...tableController,​ 
 +        tryReload,​ 
 +        pagedData: data?​.response,​ 
 +        isError, 
 +        isLoading,​ 
 +        remove 
 +    }; 
 +}
  
-// Add Authentication configuration +</code>
-builder.AddAuthenticationAndAuthorization();​+
  
-// Add Database Context +In final, se poate folosi controller hook-ul in componenta de UI.
-builder.AddBookLibraryDbContext();​+
  
-// Add MediatR +<​code>​
-builder.Services.AddMediatR(Assembly.GetExecutingAssembly());​+
  
-// Add Repositories +export const UserTable = () => { 
-builder.Services.AddBookLibraryAggregateRepositories();+    const { userId: ownUserId } = useAppSelector(x => x.profileReducer);​ 
 +    const { formatMessage } = useIntl();​ 
 +    const header = useHeader();​ 
 +    const orderMap = header.reduce((acc,​ e, i) => { return { ...acc, [e.key]: i } }, {}) as { [key: string]: number }; // Get the header column order. 
 +    const { handleChangePage,​ handleChangePageSize,​ pagedData, isError, isLoading, tryReload, labelDisplay,​ remove } = useUserTableController();​ // Use the controller hook. 
 +    const rowValues = getRowValues(pagedData?​.data,​ orderMap); // Get the row values.
  
-// Add Api Features Handlers +    return <​DataLoadingContainer isError={isError} isLoading={isLoading} tryReload={tryReload}>​ {/* Wrap the table into the loading container because data will be fetched from the backend and is not immediately available.*/
-builder.Services.AddApiFeaturesHandlers();+        <​UserAddDialog /> {/* Add the button to open the user add modal. */} 
 +        ​{!isUndefined(pagedData) && !isUndefined(pagedData?​.totalCount) && !isUndefined(pagedData?​.page) && !isUndefined(pagedData?​.pageSize&&​ 
 +            <​TablePagination // Use the table pagination to add the navigation between the table pages. 
 +                component="​div"​ 
 +                count={pagedData.totalCount} // Set the entry count returned from the backend. 
 +                page={pagedData.totalCount !== 0 ? pagedData.page - 1 : 0} // Set the current page you are on. 
 +                onPageChange={handleChangePage} // Set the callback to change the current page. 
 +                rowsPerPage={pagedData.pageSize} // Set the current page size. 
 +                onRowsPerPageChange={handleChangePageSize} // Set the callback to change the current page size.  
 +                labelRowsPerPage={formatMessage({ id: "​labels.itemsPerPage"​ })} 
 +                labelDisplayedRows={labelDisplay} 
 +                showFirstButton 
 +                showLastButton 
 +            />} 
 +        ...
  
-var app = builder.Build();​ +    </DataLoadingContainer >
- +
-// Configure the HTTP request pipeline. +
-if (app.Environment.IsDevelopment()) +
-+
-    app.UseSwagger();​ +
-    app.UseSwaggerUI();​ +
-} else +
-+
-    app.UseHttpsRedirection();​+
 } }
- 
-app.UseAuthenticationAndAuthorization();​ 
- 
-app.UseHttpLogging();​ 
- 
-app.MapControllers();​ 
- 
-app.Run(); 
 </​code>​ </​code>​
  
-<note tip>In Visual Studio, puneti cursorul peste **builder**. O sa vedeti ​ca este un obiect de tipul WebApplicationBuilder.</​note>​+Sunteti liberi ​sa folositi cele prezentate sau nu, dar consideram ​ca aceasta abordare poate sa va ajute in organizarea codului si mentenabilitatea acestuia.
  
-Obiectul **builder** este folosit pentru configurarea serverului. Configurarea serverului implica **injectarea** obiectelor necesare la runtime. Un exemplu este:  +===== Redux Toolkit =====
-<code c#> +
-builder.Services.AddEndpointsApiExplorer();​ +
-</​code>​ +
-Secventa de cod de mai sus face posibila descoperirea ruterlor scrise de noi de catre server. ​+
  
-<note tip>In Visual Studiopuneti cursorul peste **app**. O sa vedeti ca este un obiect de tipul WebApplication.</​note>​+Exista diferite biblioteci care implementeaza aceasta logicaReact vine la pachet cu [[https://​reactjs.org/​docs/​context.html|Context API]] si a fost istoric solutia implicita de a gestiona starea globala insa alte implementari au aparut si cea mai populara a devenit ​**Redux**.
  
-Obiectul ​**app** este folosit ​pentru ​adaugarea middlewares ​care sunt executate in momentul in care serverul primeste ​un requestImaginati-va middlewares ca serie de functii care sunt apelate una dupa alta.+Redux functioneaza intr-un mod foarte simplu, exista o stare globala initializata la incarcarea aplicatiei si pe starea globala se definesc tranzitii ca intr-un automat finit de stari. Tranzitiile se declanseza folosind ​**useDispatch** care returneaza o functie **dispatch** care poate sa trimita un obiect cu populat cu date pentru ​a declansa tranzitiile/​mutatiile de stare, tranzitii ​care sunt definite intr-un **reducer** ​care practic este un **switch** si determina ce tranzitie se efectueaza pe baza datelor trimise prin dispatchStarea efectiva se poate accesa prin **useSelector** pentru a returna ​parte din starea globala, variabilele returnate se actualizeaza in mod automat oricunde apar in aplicatie cand se modifica starea globala prin functia ​de dispatch.
  
-<code c#> +Chiar si asa, Redux simplu este destul ​de greu de folosit deoarece trebuie definite ​de dezvoltatori toti reduceri de mana cu fiecare tranzitieAstfel peste Redux a fost creata biblioteca de **[[https://​redux-toolkit.js.org/​|Redux Toolkit]]** care se poate instala prin npm:
-app.UseHttpLogging();​ +
-</​code>​ +
-Secventa ​de cod de mai introduce logging ​de request la fieacare request, sub forma unui middleware. +
-==== Dependency Injection ====+
  
-.NET foloseste foarte mult **[[https://​docs.microsoft.com/​en-us/aspnet/​core/​fundamentals/​dependency-injection?​view=aspnetcore-6.0|dependency injection]]**. Si obiectul **builder**,​ prin apelarea acelor metode, de fapt introduce obiecte in containerul de depdendente folosind dependency injection.+''​npm install react-redux @reduxjs/toolkit''​
  
-Dependency Injection, pe scurt, se refera la **injectarea obiectelor de care este nevoie**, fara sa fie nevoie sa fie construite pe loc+Redux Toolkit expune metode mai simple de a crea reduceri si de a gestiona starea globala prin stari mai mici denumite ca **slice**. Ca exemplu aveti mai jos cum se poate defini un slice si un reducer.
  
-Propunem urmatorul scenariu: Obiectul A foloseste Obiectul B. Acest lucru se poate face in doua moduri:+<​code>​ 
 +/** 
 + * Use constants to identify keys in the local storage.  
 + */ 
 +const tokenKey = "​token";​
  
-  - Obiectul A instantiaza Obiectul B +/** 
-  ​- Obiectul A obtine Obiectul B din containerul de dependentedirect instantiat+ * This decodes the JWT token and returns the profile. 
 + */ 
 +const decodeToken = (token: string | null): ProfileState => { 
 +  ​let decoded = token !== null ? jwtDecode<​{ nameid: stringname: string, email: string, exp: number }>​(token) : null; 
 +  const now = Date.now() / 1000;
  
-<note tip>​Scenariul 2 se refera la Dependency InjectionIn loc ca obiectele de care se depinde, sa fie instantiate,​ ele sunt direct preluate din containerul de depenente.</note>+  if (decoded?.exp && decoded.exp now) { 
 +    decoded = null; 
 +    token = null; 
 +    localStorage.removeItem(tokenKey);​ 
 +  }
  
-Pentru a folosi dependency injection in .NET este nevoie de 2 lucruri:+  return { 
 +    loggedIn: token !== null, 
 +    token: token ?? null, 
 +    userId: decoded?.nameid ?? null, 
 +    namedecoded?​.name ?? null, 
 +    email: decoded?​.email ?? null, 
 +    exp: decoded?​.exp ?? null 
 +  }; 
 +};
  
-   - Obiectele trebuie ​**injectate** in containerul de dependente+/** 
-   - Cand este nevoie de un obiect, acesta trebuie mentionat in constructorul obiectului parinte.+ The reducer needs a initial state to avoid non-determinism
 + */ 
 +const getInitialState = (): ProfileState => decodeToken(localStorage.getItem(tokenKey));​ // The initial state doesn'​t need to come from the local storage but here it is necessary to persist the JWT token.
  
-<note tip>​Containerul de dependente implicit din .NET este accesat prin obiectul ​**IServiceCollection**, obiect copil al **WebApplicationBuilder**.</note>+/**  
 + The Redux slice is a sub-state of the entire Redux stateRedux works as a state machine and the slices are subdivisions of it for better management.  
 + *
 +export const profileSlice = createSlice({ 
 +  name: "​profile",​ // The name of the slice has to be unique. 
 +  initialState:​ getInitialState(), ​// Add the initial state 
 +  reducers: { 
 +    setToken: (_, action: PayloadAction<​string>) => { // The payload is a wrapper to encapsulate the data sent via dispatch. Here the token is received, saved and a new state is created. ​    
 +      localStorage.setItem(tokenKey,​ action.payload);​
  
-<note tip>​Obiectele care sunt injectate, pot fi injectate in 3 cicluri de viata: **transient**,​ **scoped** si **singletone**.</note> +      return decodeToken(action.payload); ​// You can either return a new state or change it via the first parameter that is the current state. 
-<note tip>​Chiar daca acum pare putin ambiguuo sa vedeti exemple concrete in cod.</​note>+    }, 
 +    resetProfile:​ () ={ // This removes the token from the storage and resets the state. 
 +      localStorage.removeItem(tokenKey);​
  
-==== Metode Extensie ==== +      return { 
- +        ​loggedIn:​ false
-Alt principiu folosit in .NETin special pentru configurari,​ este [[https://​docs.microsoft.com/​en-us/​dotnet/​csharp/​programming-guide/​classes-and-structs/​extension-methods|Metoda extensie]]. ​ +        tokennull, 
- +        ​userId: null, 
-Metodele extensie reprezinta o metoda care este scrisa in numele altui obiect. Exemplu: +        ​namenull, 
-<code c#> +        emailnull, 
-using Microsoft.AspNetCore.Authentication.JwtBearer;​ +        expnull 
- +      };
-namespace BookLibrary.Api.Authorization +
-+
-    public static class AuthorizationExtensions +
-    { +
-        ​public static void AddAuthenticationAndAuthorization(this WebApplicationBuilder builder) +
-        ​+
-            builder.Services.AddAuthentication(options => +
-            { +
-                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;​ +
-                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;​ +
-            }).AddJwtBearer(options => +
-            { +
-                options.Authority = builder.Configuration["​Auth:Authority"​];​ +
-                ​options.Audience = builder.Configuration["​Auth:Audience"​];​ +
-            }); +
- +
-            // Add Authorization configuration +
-            builder.Services.AddAuthorization(configure => +
-            { +
-                configure.AddPolicy("​AdminAccess",​ policy => policy.RequireClaim("​permissions",​ "​book-library:admin"​));​ +
-            }); +
-        } +
-         +
-        //...+
     }     }
-}+  ​} 
 +});
  
-</code>+export const {  
 +  setToken, 
 +  resetProfile 
 +} = profileSlice.actions; ​// Export the slice actions, they are used to wrap the data that is send via the dispatch function to the reducer.
  
-<note tip>​Observati cum singurul parametru al acestei metode este **WebApplicationBuilder builder**, care este prefixat cu **this**Prefixul cu this marcheaza faptul ca aceasta metoda este una de extensie pentru obiectul de tipul**WebApplicationBuilder**.<​/note>+export const profileReducer = profileSlice.reducer; ​// Export the reducer.
  
-Utilizarea metodelor de extensie ajuta prin imbogatirea functionalitatilor unui obiect la care nu exista acces. In exemplul de mai sus, am adaugat functionalitate obiectului **builder**,​ pentru a putea configura aplicatia+export const store = configureStore({ 
 +  reducer: { 
 +    profileReducer // Add more reducers here as needed. 
 +  } 
 +});
  
-In cadrul proiectului nostru, metodele de extensie sunt grupate in functie de scopul lor: 
- 
-{{:​pw:​laboratoare:​solutionexplorerapiextensions.png?​600|}} 
- 
-Folderul **Authorization** contine metode de extensie pentru configurarea autentificarii si autorizarii la nivel de API, precum si pentru a extrage informatii din utilizatorul autentificat. 
- 
-Folderul **Infrastructure** contine metode de extensie pentru **dependency injection** a repository-urilor definite in **Core** (sub forma de interfete) si implementate in **Infrastructture**. 
-<code c#> 
-public static void AddBookLibraryAggregateRepositories(this IServiceCollection services) 
-{ 
-    services.AddTransient<​IBooksRepository,​ BooksRepository>​();​ 
-    services.AddTransient<​IUsersRentalsRepository,​ UsersRentalsRepository>​();​ 
-} 
 </​code>​ </​code>​
-Pe langa injectarea repository-urilor,​ am configurat si **contextul bazei de date**, instruind serverul sa foloseasca **Microsoft SQL Server** cu configuratia scrisa de noi in **appsettings.json**. 
-<code c#> 
-public static void AddBookLibraryDbContext(this WebApplicationBuilder builder) 
-{ 
-    builder.Services.AddDbContext<​BookLibraryContext>​(opt => 
-        opt.UseSqlServer(builder.Configuration.GetConnectionString("​BookLibraryDb"​)));​ 
-} 
-</​code>​ 
-<note tip>​**Reminder**:​ Acest context a fost folosit in repository-uri,​ in cadrul proiectului **Infrastructure**. Motivul pentru care a putut fi folosit este ca a fost configurat si injectat aici.</​note>​ 
  
-<code c#> +Mai jos puteti folosi Redux pentru a face disponibila starea globala in aplicatie la descendentii componentei **Provider**.
-public class BooksRepository : IBooksRepository +
-+
-    private readonly BookLibraryContext context;+
  
-    public BooksRepository(BookLibraryContext context+<​code>​ 
-    { +ReactDOM.createRoot(document.getElementById('​root'​as HTMLElement).render( 
-        this.context = context; +  <​React.StrictMode>​ 
-    } +    {/* The Provider adds the storage for Redux*/} 
-    // ... +    ​<​Provider store={store}
-}+      ... 
 +    ​</Provider>​ 
 +  </React.StrictMode>​ 
 +)
 </​code>​ </​code>​
  
-Folderul **Web** contine metode de extensie ​pentru ​Swagger si **features scrise de noi**o exceptie custom ​si un filtru de exceptie pentru exceptia noastra.  +Dupa definirea ​pentru ​Reduxstarea se poate fi folosita ​si modificata ca in urmatorul exemplu.
-<code c#> +
-using BookLibrary.Api.Features.Books;​ +
-using BookLibrary.Api.Features.Metrics;​ +
-using BookLibrary.Api.Features.Profile;​ +
-using BookLibrary.Api.Features.Rentals;+
  
-namespace BookLibrary.Api.Web 
-{ 
-    public static class ApiFeaturesExtensions 
-    { 
-        public static void AddApiFeaturesHandlers(this IServiceCollection services) 
-        { 
-            // Add Book Handlers 
-            services.AddBooksHandlers();​ 
- 
-            // Add Profile Handlers 
-            services.AddProfilesHandlers();​ 
- 
-            // Add Rentals Handlers 
-            services.AddRentalsHandlers();​ 
- 
-            // Add Metrics Handlers 
-            services.AddMetricsHandlers();​ 
-        } 
-    } 
-} 
-</​code>​ 
- 
-<note tip>In metoda de mai sus am instruit obiectul **services** sa apeleze metodele de extensie pentru fiecare **feature** scris in parte. Aceste metode de extensie sunt definite in cadrul fiecarui feature.</​note>​ 
-Exemplu: 
-<code c#> 
-using BookLibrary.Api.Features.Books.AddBook;​ 
-using BookLibrary.Api.Features.Books.ViewAllBooks;​ 
-using BookLibrary.Api.Features.Books.ViewStatusAboutBook;​ 
- 
-namespace BookLibrary.Api.Features.Books 
-{ 
-    internal static class BooksModule 
-    { 
-        internal static void AddBooksHandlers(this IServiceCollection services) 
-        { 
-            services.AddTransient<​IAddBookCommandHandler,​ AddBookCommandHandler>​();​ 
-            services.AddTransient<​IViewAllBooksQueryHandler,​ ViewAllBooksQueryHandler>​();​ 
-            services.AddTransient<​IViewStatusAboutBookQueryHandler,​ ViewStatusAboutBookQueryHandler>​();​ 
-        } 
-    } 
-} 
-</​code>​ 
-===== Informatii generale ===== 
-  * Hypertext Markup Language a fost propus in 1989 de Tim Berners-Lee,​ pentru organizatia stiintifica CERN (Elvetia) 
-  * HTML descrie structura unei pagini Web. Elementele HTML sunt reprezentate cu tag-uri 
-  * Browser-ul nu va afisa tagurile HTML, dar le va utiliza in vederea afisarii continutului paginii 
-  * Cascading Style Sheet este maniera preferata prin care atasam proprietati grafice de randare elementelor din DOM 
-  * Putem include declarații CSS în 3 moduri: inline, intern (in <​head>​) sau extern (versiunea recomandata) ​ 
-  * In cazul unui fisier separat, acesta trebuie legat in head prin tagul: <​code>​ <link rel="​stylesheet"​ type="​text/​css"​ href="​mystylesheet.css">​ </​code>​ 
- 
-===== Structura HTML ===== 
-Pentru crearea unei pagini cu textul "Hello World" cu un link spre Google, vom scrie: ​ 
 <​code>​ <​code>​
-<​!DOCTYPE html> +  const { token } = useSelector(x =x.profileReducer);​ /You can use the data form the Redux storage. 
-<​html>​ +  ​const dispatch = useDispatch();​ 
-  <​head>​ +  ​const logout = useCallback(() ={ 
-    <​title>​Titlu<​/title> +    ​dispatch(resetProfile()); ​// Use the reducer action to create the payload for the dispatch
-  ​</​head>​ +  ​}, []);
-  ​<body+
-  <a href="​https:​//www.google.com">​Hello World!</​a>​ +
-  ​</​body>​ +
-</​html>​+
 </​code>​ </​code>​
  
-==== Explicatii: ==== +Pentru mai multe detalii va rugam sa va uitati ​in aplicatia demo de pe gitlab-ul nostru.
-  * Declaratia **DOCTYPE** este prima linie din fisierul HTML si specifica versiunea HTML in care a fost scrisa pagina +
-  * Minimul necesar pentru un document valid consta intr-un tag **title**; cu toate acestea, recomandarea este ca documentul ​sa contina cel putin un tag **html**, cu un singur **head**, un **title** si un **body** +
-  * **href** este un atribut si aduce informatii aditionale despre elementul HTML caruia ii este asignat  +
-  * Ca structura, atributele vin in perechi cheie/​valoare,​ precum: name=”value”,​ asa cum se poate observa in exemplul ​de sus+
  
-==== Tag-uri HTML uzuale ​====+===== Formulare =====
  
-^           ​Pentru introducere text           ^^ +De multe ori o sa aveti nevoie de a crea formulare pentru a adauga/​schimba date pe backend. In HTML aveti multe elemente care functioneaza ca input-uri pentru formulareex. selecttextboxcheckbox etcPentru React bibliotecile ​de UI cum este si Material UI expun aceste ​input-uri deja stilizate si cu atribute care va pot ajuta pentru a controla componentele respective.
-| article ​   | Articol (tag specific HTML5) ​  ​| ​  +
-| p | Paragraf | +
-| h1-6 | Titlu sectiuneierarhizat pe 6 niveluri | +
-| a | Link catre o alta adresasau un element din acelasi documentspecificate in atributul **href**; poate contine atributul **target** care specifica modul in care se va deschide noua adresa (i.e. inlocuind pagina deschisa curenta, sau creand o alta noua) | +
-| ol, ul | Lista ordonata, respectiv ne-ordonata;​ ambele contin enumerari ​de taguri **li** | +
-| dl | Lista de grupuri nume-valoare (sau definitii), unde **dt** specifica numele ​si este urmat de **dd** care specifica valoarea | +
-| pre, code | Afiseaza text pre-formatat,​ respectiv text care reprezinta cod intr-un limbaj de programare; ​aceste ​tag-uri indica clientului sa nu incerce sa formateze sau sa evalueze posibilele expresii din continutul tag-urilor, ci sa le afiseze asa cum sunt |+
  
-^           ​Pentru crearea tabelelor ​          ^^ +Problemele ​in definirea formularelor este atat controla componentelor din formular cat si validarea formularuluine dorim ca formular sa valideze datele pana sa fie trimis catre backend cat si sa se schimbe la diferite actiuni ale utilizatorului cum ar fi pentru afisarea erorilor ​sau pentru ascunderea unur campuri. Pentru controlarea componentelor vom folosi ​**[[https://​react-hook-form.com/​|react-hook-form]]** iar pentru validarea formularului [[https://​www.npmjs.com/​package/​yup|Yup]]:
-| table | Tabel nou| +
-| tr | Table row; linie noua (poate fi continuta ​in **thead****tbody** ​sau **tfoot**) ​| +
-| td | Table data; celula noua | +
-| th | Table head; celula din capul de tabel | +
-| thead | Cap de tabel | +
-| tbody | Continut tabel | +
-| tfoot | Subsol tabel | +
-| colgroup | Grup de coloane folosite pentru a crea diviziuni intr-un tabel; contine tag-uri de tipul **col** si este inclus direct in tag-ul **table** |+
  
-^           ​Formulare ​          ^^ +''​npm install react-hook-form yup @hookform/​resolvers''​
-| form | Formular; contine un atribut **action** si un atribut **method** specificand parametrii cererii HTTP ce se va genera in urma trimiterii datelor din formular| +
-| input | Camp de interactiune cu utilizatorul;​ contine un atribut **name** prin care se specifica denumirea parametrului ce va fi trimis catre server si **type** prin care se specifica tipul campului | +
-| select | Camp de selectie a unei valori dintr-o lista de tag-uri **option** | +
-| label | Text afisat in dreptul unui **input**; contine un atribut **for** pentru a specifica input-ul corespunzator | +
-| fieldset | Subdiviziune a unui formular; poate contine **legend** pentru a specifica titlul subdiviziunii | +
-| button | Buton generic; preferat unui **input** de tipul **button** | +
-| textarea | Camp pentru introducerea unui text extins |+
  
-<​note>​ +Folosind ​**useForm** putem obtine starea si actiuni de modificare pe starea unui formular dandu-se o anumita schema ​pentru ​formular iar cu yup putem definii o schema ​de validare ​pentru ​starea formularului care printr-un **resolver** va impiedica formularul sa fie submis daca nu se respecta schema de validare si va returna mesaje de eroare. In exemplul de mai jos aveti definirea unui formular simplu unde se defineste schema de validare si campurile ​formularului ​intr-un controller.
-Tipul unui input poate fi:  +
-  ​text, password - contine text, respectiv text mascat pentru introducerea de parole +
-  ​radio, checkbox - contine o optiune da/nu, exclusiva, respectiv non-exclusiva +
-  ​file - se foloseste ​pentru ​incarcarea ​de fisiere +
-  * hidden - se foloseste ​pentru ​a trimite ​un parametru fara a fi vizualizat de utilizator +
-  ​button,​submit,​ reset - reprezinta buton generic, respectiv special pentru trimiterea sau golirea ​formularului+
  
-</note>+<code> 
 +/** 
 + * Use a function to return the default values of the form and the validation schema. 
 + * You can add other values as the default, for example when populating the form with data to update an entity in the backend. 
 + */ 
 +const getDefaultValues = (initialData?:​ { email: string }) =
 +    const defaultValues = { 
 +        email: "",​ 
 +        password: ""​ 
 +    };
  
-^           ​Tag-uri media           ^^ +    if (!isUndefined(initialData)) { 
-| img | Afiseaza o imagine; sunt necesare atributele **src**pentru a preciza URL-ul imaginiisi **alt** pentru a preciza textul afisat in lipsa imaginii| +        ​return { 
-| video  | Afiseaza un filmeste necesar atributul src**,** pentru a preciza sursa video; continutul tag-ului poate contine o maniera alternativa de a afisa filmul, in caz ca tag-ul nu este suportat de navigator |+            ...defaultValues, 
 +            ...initialData
 +        }; 
 +    }
  
-^           ​Sectiuni ​          ^^ +    return defaultValues;​ 
-| div, span | Sectiuni dintr-un document HTML, fara o semantica speciala, cu rolul formatarii si pozitionarii selective | +};
-| header, nav, section, aside, footer ​ | Sectiuni specifice HTML5 pentru atribuirea de semantica diverselor sectiuni tipice dintr-un document |+
  
-^           Alte tag-uri ​          ^^ +/** 
-| script | Permite introducerea unui script rulat pe client, intr-un limbaj suportat; poate aparea in **head** sau in **body** | + Create a hook to get the validation schema. 
-| link | Permite definirea unei legaturi intre document si o sursa externa, fiind utilizat si pentru atribuirea de caracteristici CSS | + */ 
-| meta | Permite introducerea de informatii despre document ​(e.g. setul de caracter utilizat, cuvinte cheie, scurta descriere, etc.| +const useInitLoginForm = () => { 
-| iframe | Permite introducerea unui alt document HTML intr-o sectiune a documentului curentnoul document se poate afla pe un alt server |+    const { formatMessage } = useIntl(); 
 +    const defaultValues = getDefaultValues();
  
-<​note>​ Pentru a vizualiza lista completa ​tag-urilor HTMLaccesati [[https://www.w3schools.com/TAGS/default.ASP | w3schools]]<​/note>+    const schema = yup.object().shape({ // Use yup to build the validation schema of the form. 
 +        email: yup.string() // This field should be string. 
 +            .required(formatMessage( // Use formatMessage to get the translated error message. 
 +                { id: "​globals.validations.requiredField"​ }, 
 +                { 
 +                    fieldNameformatMessage({ ​// Format the message with other translated strings. 
 +                        id: "​globals.email",​ 
 +                    }), 
 +                })) // The field is required and needs a error message when it is empty. 
 +            .email() // This requires the field to have a email format. 
 +            .default(defaultValues.email), ​// Add a default value for the field. 
 +        password: yup.string() 
 +            .required(formatMessage( 
 +                { id: "​globals.validations.requiredField"​ }, 
 +                { 
 +                    fieldName: formatMessage({ 
 +                        id: "​globals.password",​ 
 +                    }), 
 +                })) 
 +            .default(defaultValues.password),​ 
 +    });
  
-===== CSS =====+    const resolver ​yupResolver(schema);​ // Get the resolver.
  
-**CSS** (Cascading Style Sheets) este un limbaj de stilizare al elementelor HTML.+    return { defaultValues,​ resolver }; // Return the default values and the resolver. 
 +}
  
-Formatul unei reguli CSS este urmatorul+/** 
-{{ :pw:laboratoare:​css_example.png |}} + * Create a controller hook for the form and return any data that is necessary for the form. 
-<note important>​ + */ 
-  * Selectorul face referire la elementul HTML peste care se aplica stilul +export const useLoginFormController = ()LoginFormController => { 
-  * Selectorul poate fi id, nume de clasa sau nume de tag +    ​const ​formatMessage } = useIntl();​ 
-  * Blocul "​declaration"​ contine una sau mai multe perechi proprietate-valoare separate prin ";" +    const { defaultValues,​ resolver } = useInitLoginForm();​ 
-  * Fiecare "​declaration"​ include o proprietate si o valoare, separate prin ":" +    const { redirectToHome } = useAppRouter();​ 
-  * Blocurile ​"declaration"​ incep si se incheie prin acolade +    const loginMutation{ mutation, keymutationKey ​} } = useLoginApi();​ 
-</​note>​+    const { mutateAsync:​ login, status } = useMutation([mutationKey],​ mutation); 
 +    const queryClient = useQueryClient();​ 
 +    const dispatch = useDispatch();​ 
 +    const submit = useCallback((data:​ LoginFormModel) => // Create a submit callback to send the form data to the backend. 
 +        login(data).then((result) => { 
 +            dispatch(setToken(result.response?​.token ?? ''​))
 +            ​toast(formatMessage({ id: "notifications.messages.authenticationSuccess" ​})); 
 +            ​redirectToHome();​ 
 +        }), [login, queryClient,​ redirectToHome,​ dispatch]);
  
-==== Selectorul id (#) ==== +    const { 
-  * Faciliteaza selectarea unui element HTML specific +        register, 
-  * Id-ului unui element trebuie sa fie unic intr-o pagina si identifica un unic element din aceasta +        handleSubmit,​ 
-  * Se utilizeaza "​**#​**"​ inaintea numelui selectorului pentru a marca faptul ca acesta vizeaza un element cu id-ul dat +        formState: { errors } 
-  * Id nu poate incepe cu un numar+    } useForm<​LoginFormModel>​({ // Use the useForm hook to get callbacks and variables to work with the form. 
 +        ​defaultValues,​ // Initialize the form with the default values. 
 +        ​resolver // Add the validation resolver. 
 +    });
  
-==== Selectorul class (.) ==== +    return { 
-  * Faciliteaza selectarea mai multor elemente avand ca atribut o anumita clasa +        actions: { // Return any callbacks needed to interact with the form
-  * Se utilizeaza "**.**" inaintea numelui selectorului pentru a selecta elemente dintr-o anumita clasa +            ​handleSubmit,​ // Add the form submit handle. 
-  * Numele clasei nu poate incepe cu un numar +            ​submit,​ // Add the submit handle that needs to be passed to the submit handle
- +            ​register // Add the variable register to bind the form fields in the UI with the form variables. 
-<note tip>​Selectorii pot fi grupati+        }, 
-<​code>​ +        ​computed{ 
-h1, .myClass#myId { +            ​defaultValues, 
-    ​text-aligncenter; +            isSubmitting:​ status === "​loading"​ // Return if the form is still submitting or nit. 
-    ​color: red;+        }
 +        state
 +            errors // Return what errors have occurred when validating the form input. 
 +        } 
 +    ​}
 } }
 </​code>​ </​code>​
  
-</​note>​+Pentru componenta de UI trebuie folosit formularul in interiorul unui element de tip **form** unde se specifica ca atribut functia de submitere. In interiorul formularului se leaga variabilele la input-urile de form si ca sa se submita trebuie sa existe un buton de tip "​submit"​ care va apela functia specificata in form cu datele din formular asa cum aveti ca exemplu mai jos.
  
-==== Combinarea selectorilor ==== 
 <​code>​ <​code>​
-/* reuniune */ +export const LoginForm = () => { 
-h1, h2 +    ​const ​formatMessage } = useIntl(); 
-  /* stilul se aplica elementelor h1 si h2 */ +    const { state, actions, computed } = useLoginFormController(); ​// Use the controller.
-}+
  
-/* intersectie ​*/ +    return <form onSubmit={actions.handleSubmit(actions.submit)}>​ {/* Wrap your form into a form tag and use the handle submit callback to validate the form and call the data submission. ​*/} 
-h1.titlu +        <​Stack spacing={4} style={{ width: "​100%"​ }}> 
-  /* stilul se aplica elementelor h1 care au clasa titlu */ +            <​ContentCard title={formatMessage({ id: "​globals.login" })}> 
-+                <Grid container item direction="​row"​ xs={12} columnSpacing={4}>​ 
- +                    <​Grid container item direction="​column"​ xs={12} md={12}>​ 
-/* descendenta ​*/ +                        <​FormControl  
-.antet h1 +                            fullWidth 
-  /* stilul se aplica elementelor h1 care au parinte un element cu clasa antet */ +                            error={!isUndefined(state.errors.email)} 
-+                        > {/* Wrap the input into a form control and use the errors to show the input invalid if needed. ​*/} 
- +                            <​FormLabel required>​ 
-/* descendenta directa ​*/ +                                <​FormattedMessage id="​globals.email"​ /> 
-.antet h1 +                            </​FormLabel>​ {/* Add a form label to indicate what the input means. ​*/} 
-  /* stilul se aplica elementelor h1 care au parinte **direct** un element ​cu clasa antet */ +                            <​OutlinedInput 
-}+                                {...actions.register("​email"​)} // Bind the form variable to the UI input. 
 +                                placeholder={formatMessage( 
 +                                    { id: "​globals.placeholders.textInput"​ }, 
 +                                    { 
 +                                        fieldName: formatMessage({ 
 +                                            id: "​globals.email",​ 
 +                                        }), 
 +                                    })} 
 +                                autoComplete="​username"​ 
 +                            /> {/* Add a input like a textbox shown here. */} 
 +                            <​FormHelperText 
 +                                hidden={isUndefined(state.errors.email)
 +                            > 
 +                                ​{state.errors.email?​.message} 
 +                            </​FormHelperText>​ {/* Add a helper text that is shown then the input has a invalid value. ​*/} 
 +                        </​FormControl>​ 
 +                    </​Grid>​ 
 +                    <Grid container item direction="​column"​ xs={12} md={12}>​ 
 +                        <​FormControl 
 +                            fullWidth 
 +                            error={!isUndefined(state.errors.password)} 
 +                        ​> 
 +                            <​FormLabel required>​ 
 +                                <​FormattedMessage id="​globals.password"​ /> 
 +                            </​FormLabel>​ 
 +                            <​OutlinedInput 
 +                                type="​password"​ 
 +                                ​{...actions.register("​password"​)} 
 +                                ​placeholder={formatMessage( 
 +                                    { id: "​globals.placeholders.textInput"​ }, 
 +                                    { 
 +                                        fieldName: formatMessage({ 
 +                                            id: "​globals.password",​ 
 +                                        }), 
 +                                    })} 
 +                                autoComplete="​current-password"​ 
 +                            /> 
 +                            <​FormHelperText 
 +                                hidden={isUndefined(state.errors.password)} 
 +                            > 
 +                                {state.errors.password?​.message} 
 +                            </​FormHelperText>​ 
 +                        </​FormControl>​ 
 +                    </​Grid>​ 
 +                </​Grid>​ 
 +            </​ContentCard>​ 
 +            <Grid container item direction="​row"​ xs={12} className="​padding-top-sm">​ 
 +                <Grid container item direction="​column"​ xs={12} md={7}></​Grid>​ 
 +                <Grid container item direction="​column"​ xs={5}>​ 
 +                    <Button type="​submit"​ disabled={!isEmpty(state.errors) || computed.isSubmitting}>​ {/* Add a button with type submit to call the submission callback if the button is a descended of the form element*/
 +                        {!computed.isSubmitting && <​FormattedMessage id="​globals.submit"​ />} 
 +                        {computed.isSubmitting && <​CircularProgress />} 
 +                    </​Button>​ 
 +                </​Grid>​ 
 +            </​Grid>​ 
 +        </​Stack>​ 
 +    </​form>​ 
 +};
 </​code>​ </​code>​
  
-<note important>​ +===== Resurse utile =====
-Daca doua sau mai multe reguli se aplica unui singur element, clientul va alege regula cea mai [[https://​www.w3schools.com/​css/​css_specificity.asp | specifica]]. +
-</​note>​ +
- +
-===== Exercitii ​===== +
-Avand sablonul de mai jos, creati o pagina HTML specifica unui articol de blog (aveti libertatea de a alege tema blogului):​ +
- +
-{{ :​pw:​laboratoare:​html_blog.png |}} +
- +
-  - [2p] Introduceti in sectiunea #main, datele specifice unui articol: titlul, descrierea articolului,​ insertii media (o imagine & un video) si doua comentarii text. Folositi taguri HTML5. Includeti in <​head>​ meta-date (data publicarii, cuvinte cheie). +
-  - [2p] Creati in sectiunea #form, un formular in care utilizatorii pot lasa feedback: +
-      * Text input selector (dropdown): pentru gradul de aprobare al articolului (foarte bine, bine, neutru, satisfacator si nesatisfacator);​ +
-      * Checkbox-uri pentru acceptarea termenilor & notificare email; +
-      * Optiunea tip text (textarea), pentru comentarii;​ +
-  - [1p] Adaugati in sectiunea #footer copyright-ul si datele de contact +
-  - [1p] Adaugati in sectiunea #nav un **tabel** cu linkuri catre blog-uri asemanatoare (legaturi catre diverse pagini web). +
-  +
-Creati un fisier .css in care definiti (conform sablonului) design-ul articolului vostru si atasati-l documentului HTML: +
-  - [0.5p] Intrati pe Google Fonts si alegeti minim 2 fonturi diferite pentru sectiunile prezente in sablon (#​container,​ #header, #main, #nav etc): +
-  - [1p] Modificati pagina web astfel incat sectiunile sa respecte alinierea, culorile, dimensiunea (800px pentru #container etc.) +
-  - [1p] Stilizati tabelul din #nav astfel incat liniile pare sa fie colorate cu gri, iar cele impare cu verde. Nu folositi selectorii class sau id pentru acest exercitiu (hint: even and odd rules).  +
-  - [1p] Cu ajutorul FontAwesome,​ adaugati imagini pentru fiecare intrare din tabelul din #nav +
-  - [0.5p] Validati pagina HTML cu W3Validator si reparati eventualele erori/​warning-uri.+
  
 +  * Aplicatia [[https://​gitlab.com/​mobylabwebprogramming/​reactfrontend|demo]] a noastra
 +  * [[https://​redux-toolkit.js.org/​|Redux Toolkit]]
 +  * [[https://​www.npmjs.com/​package/​yup|Yup]]
 +  * [[https://​react-hook-form.com/​|react-use-form]]
pw/laboratoare/06.1651051126.txt.gz · Last modified: 2022/04/27 12:18 by alexandru.hogea
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0