انقیاد خصوصیت ها در angular 2
import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; @Injectable() export class AppConfigService { private config: IAppConfig; constructor(private http: HttpClient) { } loadClientConfig(): Promise<any> { return this.http.get<IAppConfig>("assets/client-config.json") .toPromise() .then(config => { this.config = config; console.log("Config", this.config); }) .catch(err => { return Promise.reject(err); }); } get configuration(): IAppConfig { if (!this.config) { throw new Error("Attempted to access configuration property before configuration data was loaded."); } return this.config; } } export interface IAppConfig { apiEndpoint: string; loginPath: string; logoutPath: string; refreshTokenPath: string; accessTokenObjectKey: string; refreshTokenObjectKey: string; adminRoleName: string; }
و تغییر ماژول CoreModule به شکل زیر :
import { NgModule, Optional, SkipSelf, APP_INITIALIZER } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterModule } from "@angular/router"; import { HTTP_INTERCEPTORS } from "@angular/common/http"; // import RxJs needed operators only once import "./services/rxjs-operators"; import { HeaderComponent } from "./component/header/header.component"; import { AuthGuard } from "./services/auth.guard"; import { AuthInterceptor } from "./services/auth.interceptor"; import { AuthService } from "./services/auth.service"; import { AppConfigService } from "./services/app-config.service"; import { BrowserStorageService } from "./services/browser-storage.service"; @NgModule({ imports: [CommonModule, RouterModule], exports: [ // components that are used in app.component.ts will be listed here. HeaderComponent ], declarations: [ // components that are used in app.component.ts will be listed here. HeaderComponent ], providers: [ // global singleton services of the whole app will be listed here. BrowserStorageService, AppConfigService, AuthService, AuthGuard, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: APP_INITIALIZER, useFactory: (config: AppConfigService) => () => config.loadClientConfig(), deps: [AppConfigService ], multi: true } ] }) export class CoreModule { constructor( @Optional() @SkipSelf() core: CoreModule) { if (core) { throw new Error("CoreModule should be imported ONLY in AppModule."); } } }
Cannot instantiate cyclic dependency! ApplicationRef ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
تمرین 1 - ایجاد فرم ثبت نام
میخواهیم به برنامه، فرم ثبت نام را که حاوی سه فیلد نام کاربری، کلمهی عبور و نام است، اضافه کنیم. نام کاربری باید از نوع ایمیل باشد. بنابراین اعتبارسنجی مرتبطی نیز باید برای این فیلد تعریف شود. کلمهی عبور وارد شده باید حداقل 5 حرف باشد. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشدهاست، باید دکمهی submit فرم، غیرفعال باقی بماند. لینک ورود به این فرم نیز باید به منوی راهبری سایت اضافه شود.
برای حل این تمرین، فایل جدید registerForm.jsx را در پوشهی components ایجاد میکنیم و سپس توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت RegisterForm را ایجاد کرده و سپس آنرا به صورت زیر تکمیل میکنیم:
- ابتدا در فایل app.js، پس از import ماژول آن:
import RegisterForm from "./components/registerForm";
<Route path="/register" component={RegisterForm} />
<NavLink className="nav-item nav-link" to="/register"> Register </NavLink>
import Joi from "@hapi/joi"; import React from "react"; import Form from "./common/form"; class RegisterForm extends Form { state = { data: { username: "", password: "", name: "" }, errors: {} }; schema = { username: Joi.string() .required() .email({ minDomainSegments: 2, tlds: { allow: ["com", "net"] } }) .label("Username"), password: Joi.string() .required() .min(5) .label("Password"), name: Joi.string() .required() .label("Name") }; doSubmit = () => { // Call the server console.log("Submitted"); }; render() { return ( <div> <h1>Register</h1> <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password", "password")} {this.renderInput("name", "Name")} {this.renderButton("Register")} </form> </div> ); } } export default RegisterForm;
- سپس state این کامپوننت را با شیءای حاوی دو خاصیت data و error، مقدار دهی اولیه میکنیم. خواص متناظر با المانهای فرم را نیز به صورت یک شیء، به خاصیت data انتساب دادهایم.
- پس از آن، خاصیت schema تعریف شدهاست؛ تا قواعد اعتبارسنجی تک تک فیلدهای فرم را به کمک کتابخانهی Joi، مطابق نیازمندیهایی که در ابتدای تعریف این تمرین مشخص کردیم، ایجاد کند.
- در ادامه، متد doSubmit را ملاحظه میکنید. این متد پس از کلیک بر روی دکمهی Register و پس از اعتبارسنجی موفقیت آمیز فرم، به صورت خودکار فراخوانی میشود.
- در آخر، تعریف فرم ثبتنام را مشاهده میکنید که نکات آنرا در قسمت قبل، با معرفی کامپوننت Form و افزودن متدهای کمکی رندر input و button به آن، بررسی کردیم و در کل با نکات بررسی شدهی در فرم لاگینی که تا به اینجا ایجاد کردیم، تفاوتی ندارد.
تمرین 2- ایجاد فرم ثبت و یا ویرایش یک فیلم
فرم جدید ثبت و ویرایش یک فیلم، نکات بیشتری را به همراه دارد. در اینجا میخواهیم در بالای لیست نمایش فیلمها، یک دکمهی new movie را اضافه کنیم تا با کلیک بر روی آن، به فرم ثبت و ویرایش فیلمها هدایت شویم. این فرم، از فیلدهای یک عنوان متنی، انتخاب ژانر از یک drop down list، تعداد موجود (بین 1 و 100) و امتیاز (بین صفر تا 10) تشکیل شدهاست. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشدهاست، دکمهی submit فرم باید غیرفعال باقی بماند. پس از ذخیره شدن این فیلم (در لیست درون حافظهای برنامه)، با مراجعهی به لیست فیلمها و انتخاب آن از لیست (با کلیک بر روی لینک آن)، باید مجددا به همین فرم، در حالت ویرایش این رکورد هدایت شویم. به علاوه اگر در بالای صفحه یک id اشتباه وارد شد، باید صفحهی «پیدا نشد» نمایش داده شود.
کامپوننت MovieForm و مسیریابی آنرا در قسمت 17، تعریف و اضافه کردیم. برای تعریف لینکی به آن، به کامپوننت movies مراجعه کرده و بالای متنی که تعداد کل آیتمهای موجود در بانک اطلاعاتی را نمایش میدهد، المان زیر را اضافه میکنیم:
import { Link } from "react-router-dom"; // ... <div className="col"> <Link to="/movies/new" className="btn btn-primary" style={{ marginBottom: 20 }} > New Movie </Link> <p>Showing {totalCount} movies in the database.</p>
سپس به کامپوننت src\components\movieForm.jsx که پیشتر آنرا اضافه کرده بودیم، مراجعه کرده و به صورت زیر آنرا تکمیل میکنیم:
import Joi from "@hapi/joi"; import React from "react"; import { getGenres } from "../services/fakeGenreService"; import { getMovie, saveMovie } from "../services/fakeMovieService"; import Form from "./common/form"; class MovieForm extends Form { state = { data: { title: "", genreId: "", numberInStock: "", dailyRentalRate: "" }, genres: [], errors: {} };
- سپس این کامپوننت نیز از کامپوننت Form ارث بری میکند تا به امکانات ویژهی آن دسترسی پیدا کند.
- در ادامه در خاصیت state، طبق روالی که در کامپوننت فرم درنظر گرفتهایم، دو خاصیت data و errors باید حضور داشته باشند. در خاصیت data، شیءای که نام خاصیتهای آن با فیلدهای فرم تطابق دارد، ذکر شدهاند. در اینجا برای ذخیره سازی اطلاعات انتخاب شدهی از drop down list مرتبط با ژانرهای سینمایی، از خاصیت genreId استفاده میشود؛ این تنها اطلاعاتی است که از کل آیتمهای یک drop down list نیاز داریم. آرایهی genres که آیتمهای این drop down list را مقدار دهی میکند، در روال componentDidMount، از سرویس مرتبطی دریافت و مقدار دهی خواهد شد.
در ادامهی کدهای کامپوننت MovieForm، کدهای schema اعتبارسنجی شیء data را ملاحظه میکنید:
schema = { _id: Joi.string(), title: Joi.string() .required() .label("Title"), genreId: Joi.string() .required() .label("Genre"), numberInStock: Joi.number() .required() .min(0) .max(100) .label("Number in Stock"), dailyRentalRate: Joi.number() .required() .min(0) .max(10) .label("Daily Rental Rate") };
اکنون به مرحلهی componentDidMount میرسیم:
componentDidMount() { const genres = getGenres(); this.setState({ genres }); const movieId = this.props.match.params.id; if (movieId === "new") return; const movie = getMovie(movieId); if (!movie) return this.props.history.replace("/not-found"); this.setState({ data: this.mapToViewModel(movie) }); }
- پس از آن، نحوهی دریافت پارامتر id مسیریابی رسیده را ملاحظه میکنید. این id اگر به "new" تنظیم شده بود، یعنی قرار است، اطلاعات جدیدی ثبت شوند. بنابراین متد جاری را خاتمه میدهیم (چون کار ادامهی این متد، مقدار دهی اولیهی تمام فیلدهای فرم، بر اساس اطلاعات شیء دریافت شدهی از سرویس فیلمها است). در غیراینصورت (و با مشخص بودن id)، با استفاده از این id و متد getMovie سرویس src\services\fakeMovieService.js، سعی خواهیم کرد تا اطلاعات شیء movie متناظری را دریافت کنیم. اگر خروجی این متد null بود، یعنی id وارد شده معتبر نیست. به همین جهت کاربر را به صفحهی not-found هدایت میکنیم. اگر دقت کنید در اینجا بجای متد push، از متد replace استفاده کردهایم. چون اگر از متد push استفاده میکردیم و کاربر بر روی دکمهی back مرورگر کلیک میکرد، دوباره به همین صفحه، با id غیرمعتبر قبلی وارد میشد و یک حلقهی بیپایان رخ میداد. همچنین به return ای هم که به همراه متد replace استفاده شده، دقت کنید. کار redirect به یک صفحهی دیگر، به معنای عدم اجرای کدهای پس از آن نیست. بنابراین اگر میخواهیم کار این متد با redirect، به پایان برسد، ذکر return الزامی است.
- در پایان این متد، خاصیت data موجود در state را به روز رسانی میکنیم؛ تا سبب رندر فرم، با اطلاعات شیء movie یافت شده گردد و چون ساختار شیء movie دریافت شدهی از سرویس، با ساختار data تعریف شدهی در state یکی نیست، نیاز به نگاشت این دو به هم، توسط متد سفارشی mapToViewModel زیر است:
mapToViewModel(movie) { return { _id: movie._id, title: movie.title, genreId: movie.genre._id, numberInStock: movie.numberInStock, dailyRentalRate: movie.dailyRentalRate }; }
در ادامهی کدهای کامپوننت فرم فیلمها، به متد doSubmit میرسیم:
doSubmit = () => { saveMovie(this.state.data); this.props.history.push("/movies"); };
در انتهای این کامپوننت نیز به متد رندر آن میرسیم:
render() { return ( <div> <h1>Movie Form</h1> <form onSubmit={this.handleSubmit}> {this.renderInput("title", "Title")} {this.renderSelect("genreId", "Genre", this.state.genres)} {this.renderInput("numberInStock", "Number in Stock", "number")} {this.renderInput("dailyRentalRate", "Rate")} {this.renderButton("Save")} </form> </div> ); }
برای تعریف متد جدید renderSelect به این صورت عمل میکنیم:
- ابتدا فایل جدید src\components\common\select.jsx را ایجاد کرده و سپس آنرا جهت نمایش یک drop down list، ویرایش میکنیم:
import React from "react"; const Select = ({ name, label, options, error, ...rest }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <select name={name} id={name} {...rest} className="form-control"> <option value="" /> {options.map(option => ( <option key={option._id} value={option._id}> {option.name} </option> ))} </select> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Select;
- پس از آن به کامپوننت src\components\common\form.jsx مراجعه کرده و متد رندر آنرا اضافه میکنیم:
import Select from "./select"; // ... class Form extends Component { // ... renderSelect(name, label, options) { const { data, errors } = this.state; return ( <Select name={name} value={data[name]} label={label} options={options} onChange={this.handleChange} error={errors[name]} /> ); } }
تمرین 3- جستجوی در لیست فیلمها
میخواهیم در بالای لیست نمایش فیلمها، یک search box را قرار دهیم تا توسط آن بتوان بر اساس عنوان وارد شده، در فیلمهای موجود جستجو کرد. همچنین این جستجو قرار است کلی بوده و حتی در صورت انتخاب ژانر خاصی از منوی کنار صفحه، باید در کل اطلاعات موجود جستجو کند. به علاوه اگر کاربر ژانری را انتخاب کرد، این text box باید خالی شود.
برای اینکار ابتدا فایل جدید src\components\searchBox.jsx را ایجاد کرده و به صورت زیر آنرا تکمیل میکنیم:
import React from "react"; const SearchBox = ({ value, onChange }) => { return ( <input type="text" name="query" className="form-control my-3" placeholder="Search..." value={value} onChange={e => onChange(e.currentTarget.value)} /> ); }; export default SearchBox;
سپس به کامپوننت movies مراجعه کرده و آنرا ذیل متن نمایش تعداد رکوردها، درج میکنیم:
<p>Showing {totalCount} movies in the database.</p> <SearchBox value={searchQuery} onChange={this.handleSearch} />
import SearchBox from "./searchBox"; //... class Movies extends Component { state = { //... selectedGenre: {}, searchQuery: "" }; handleSearch = query => { this.setState({ searchQuery: query, selectedGenre: null, currentPage: 1 }); }; handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({ selectedGenre: genre, searchQuery: "", currentPage: 1 }); };
متد handleGenreSelect را نیز اندکی تغییر دادهایم تا اگر گروهی انتخاب شد، مقدار searchQuery را خالی کند. اگر در اینجا searchQuery را به نال تنظیم میکردیم، controlled component جعبهی جستجو، تبدیل به کامپوننت کنترل نشدهای میشد و در این حالت، React، اخطار تبدیل بین این دو را صادر میکرد.
در آخر، ابتدای متد getPageData هم جهت اعمال searchQuery، به صورت زیر تغییر میکند:
getPagedData() { const { pageSize, currentPage, selectedGenre, movies: allMovies, sortColumn, searchQuery } = this.state; let filteredMovies = allMovies; if (searchQuery) { filteredMovies = allMovies.filter(m => m.title.toLowerCase().startsWith(searchQuery.toLowerCase()) ); } else if (selectedGenre && selectedGenre._id) { filteredMovies = allMovies.filter(m => m.genre._id === selectedGenre._id); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-21.zip
Building form with latest technique in Angular 2 RC.2
Introduction to Angular 2 Forms - Template Driven, Model Driven or In-Between
Learn FULL STACK Web Development in 2 HOURS! ASP.NET & Angular
Timestamps:
00:00:00 Welcome to our 2 hour FULL STACK Course!
00:01:12 What you will learn during the next 2 hours
00:03:29 Day 1 Multi-Page and Single-Page Applications, TypeScript, and Angular Components
00:21:57 Day 2 One-Way Binding and Event Binding in Angular
00:34:23 Day 3 Our Flight Booking Portal and routing in Angular
00:44:52 Day 4 The 'Search Flight' page, Design a HTML page, install Font Awesome using Node, and TypeScript interfaces
01:27:00 Day 5 ASP.NET Core REST API, SWAGGER for documenting and testing our API
01:55:51 Thanks for watching!
> npx create-react-app redux-template --template redux > cd redux-template > yarn start
import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './features/counter/counterSlice'; export default configureStore({ reducer: { counter: counterReducer, }, });
Counter Counter.module.css counterSlice.js
import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { decrement, increment, incrementByAmount, selectCount, } from './counterSlice'; import styles from './Counter.module.css'; export function Counter() { const count = useSelector(selectCount); const dispatch = useDispatch(); const [incrementAmount, setIncrementAmount] = useState(2); return ( <div> <div className={styles.row}> <button className={styles.button} aria-label="Increment value" onClick={() => dispatch(increment())} > + </button> <span className={styles.value}>{count}</span> <button className={styles.button} aria-label="Decrement value" onClick={() => dispatch(decrement())} > - </button> </div> <div className={styles.row}> <input className={styles.textbox} value={incrementAmount} onChange={e => setIncrementAmount(e.target.value)} /> <button className={styles.button} onClick={() => dispatch( incrementByAmount({ amount: Number(incrementAmount) || 0 }) ) } > Add Amount </button> </div> </div> ); }
import { createSlice } from '@reduxjs/toolkit'; export const slice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { increment: state => { // Redux Toolkit allows us to 'mutate' the state. It doesn't actually // mutate the state because it uses the immer library, which detects // changes to a "draft state" and produces a brand new immutable state // based off those changes state.value += 1; }, decrement: state => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload.amount; }, }, }); export const selectCount = state => state.counter.value; export const { increment, decrement, incrementByAmount } = slice.actions; export default slice.reducer;
- name: برای هر بخش از state، میتوانیم یک نام را تعیین کنیم و این همان عنوانی خواهد بود که میتوانید توسط Redux DevTools مشاهده کنید.
- initialValue: در اینجا میتوانیم مقادیر اولیهای را برای این بخش از state، تعیین کنیم که در مثال فوق، value به مقدار صفر تنظیم شدهاست.
- reducers: این قسمت محل تعریف actionهایی هستند که قرار است state را تغییر دهند. نکته جالب توجه این است که state در هر کدام از متدهای فوق، به ظاهر mutate شده است؛ اما همانطور که به صورت کامنت نیز نوشتهاست، در پشت صحنه از کتابخانهای با عنوان immer استفاده میکند که در عمل بجای تغییر state اصلی، یک کپی از state جدید را جایگزین state قبلی خواهد کرد.
return { ...state, models: state.models.map(c => c.model === action.payload.model ? { ...c, on: action.payload.toggle } : c ) };
state.models.forEach(item => { if (item.model === action.payload.model) { item.on = action.payload.toggle; } });
معرفی Ember
Would you like help creating single-page applications (SPAs)? Maybe you've worked with tools like jQuery and AJAX, but what about Ember? With a simple syntax and an emphasis on reuse and components, this JavaScript framework can make it very easy to create interactive pages. Get a close look at Ember, plus lots of demos and practical tips with popular experts Adam Tuliper and Christopher Harrison, in this three-hour event for the MVA community
I’ve been kicking around the idea of combining [HttpPost] and [ValidateAntiForgeryToken] in an application using authentication cookies. Both attributes typically appear together to prevent cross-site request forgeries in MVC applications using cookie based authentication. The result looks like the following.