در این مقاله قصد داریم با استفاده از جاوااسکریپت خالص، یک برنامهی ساده را با الگوی MVC انجام دهیم. این برنامه، عملیات CRUD را پیاده سازی میکند و تنها به سه فایل index.html , script.js , style.css نیاز دارد و از هیچ کتابخانه یا فریم ورک دیگری در آن استفاده نمیکنیم.
- M مخفف Model میباشد و کار مدیریت دادهها را بر عهده دارد.
- V مخفف View میباشد و وظیفهی نمایش دادهها به کاربر را بر عهده دارد.
- C مخفف Controller میباشد و پل ارتباطی بین Model و View میباشد و مدیریت درخواستها را بر عهده دارد.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>الگوی MVC در جاوااسکریپت</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="root"></div> <script src="script.js"></script> </body> </html>
*, *::before, *::after { box-sizing: border-box } html { color: #444; } #root { max-width: 450px; margin: 2rem auto; padding: 0 1rem; } form { display: flex; margin-bottom: 2rem; } [type="text"], button { display: inline-block; -webkit-appearance: none; padding: .5rem 1rem; border: 2px solid #ccc; border-radius: 4px; } button { cursor: pointer; background: #007bff; color: white; border: 2px solid #007bff; margin: 0 .5rem; } [type="text"] { width: 100%; } [type="text"]:active, [type="text"]:focus { outline: 0; border: 2px solid #007bff; } [type="checkbox"] { margin-right: 1rem; } h1 { color: #222; } ul { padding: 0; } li { display: flex; align-items: center; padding: 1rem; margin-bottom: 1rem; background: #f4f4f4; border-radius: 4px; } li span { display: inline-block; padding: .5rem; width: 250px; border-radius: 4px; border: 2px solid transparent; } li span:hover { background: rgba(179, 215, 255, 0.52); } li span:focus { outline: 0; border: 2px solid #007bff; background: rgba(179, 207, 255, 0.52) }
class Model { constructor() {} } class View { constructor() {} } class Controller { constructor(model, view) { this.model = model this.view = view } } const app = new Controller(new Model(), new View())
class Model { constructor() { // یک آرایه از اطلاعات پیش فرض this.todos = [{ id: 1, text: 'Run a marathon', complete: false }, { id: 2, text: 'Plant a garden', complete: false }, ] } // متدی برای افزودن آیتم جدید به آرایه addTodo(todoText) { const todo = { id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1, text: todoText, complete: false, } this.todos.push(todo) } // متدی برای بروزسانی آیتم مورد نظر editTodo(id, updatedText) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: updatedText, complete: todo.complete } : todo ) } // انجام میدهد filter با استفاده از متد id تابعی که عملیات حذف را بوسیله فیلد deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) } // متدی که در آن مشخص میکنیم کار مد نظرانجام شده یا خیر toggleTodo(id) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: todo.text, complete: !todo.complete } : todo ) } }
app.model.addTodo('Take a nap') console.log(app.model.todos)
در حال حاضر با هر بار reload شدن صفحه، فقط اطلاعات پیش فرض، درون آرایه todos قرار میگیرد؛ ولی در ادامه آن را در local storage ذخیره میکنیم.
برای ساختن قسمت View، از جاوااسکریپت استفاده میکنیم و DOM را تغییر میدهیم. البته اینکار را بدون استفاده از JSX و یا یک templating language انجام خواهیم داد. قسمتهای دیگر برنامه مانند Controller و Model نباید درگیر تغییرات DOM یا CSS یا عناصر HTML باشند و تمام این موارد توسط View هندل میشود. کد View به نحو زیر خواهد بود:
class View { constructor() {} // ایجاد یک المنت با کلاسهای استایل دلخواه createElement(tag, className) { const element = document.createElement(tag) if (className) element.classList.add(className) return element } // DOM انتخاب و گرفتن آیتمی خاص از getElement(selector) { const element = document.querySelector(selector) return element } }
سپس قسمت سازنده کلاس View را تغییر میدهیم و تمام المنتهای مورد نیاز را در آن ایجاد میکنیم:
- ارجاعی به المنتی با آیدی root
- تگ h1 برای عنوان
- یک form، input و دکمهای برای افزودن آیتمی جدید به آرایهی todos
- یک المنت ul برای نمایش آیتمهای todos
constructor() { // root ارجاعی به المنتی با آیدی this.app = this.getElement('#root') // عنوان برنامه this.title = this.createElement('h1') this.title.textContent = 'Todos' // فرم ، اینپوت ورودی و دکمه this.form = this.createElement('form') this.input = this.createElement('input') this.input.type = 'text' this.input.placeholder = 'Add todo' this.input.name = 'todo' this.submitButton = this.createElement('button') this.submitButton.textContent = 'Submit' // برای نمایش عناط آرایه یا همان لیست کارها this.todoList = this.createElement('ul', 'todo-list') // افزودن اینپوت ورودی و دکمه به فرم this.form.append(this.input, this.submitButton) // ایجاد شده است app که اینجا ارجاعی به آن بنام root اضافه کردن تمام آیتمهای بالا در المنتی با آیدی this.app.append(this.title, this.form, this.todoList) }
get _todoText() { return this.input.value } _resetInput() { this.input.value = '' }
displayTodos(todos){ //... }
متد displayTodos یک المنت ul و liهایی را به تعداد عناصر todos ایجاد میکند و آنها را نمایش میدهد. هر زمانکه تغییراتی مانند اضافه شدن، حذف و ویرایش در todos صورت گیرد، این متد دوباره فراخوانی میشود و لیست جدید را نمایش میدهد. محتوای متد dispayTodos به شکل زیر خواهد بود:
displayTodos(todos) { // حذف تمام نودها while (this.todoList.firstChild) { this.todoList.removeChild(this.todoList.firstChild) } // اگر هیچ آیتمی در آرایه نبود این پاراگراف با متن پیش فرض نمایش داده میشود if (todos.length === 0) { const p = this.createElement('p') p.textContent = 'Nothing to do! Add a task?' this.todoList.append(p) } else { // وعناصرمربوطه را ایجاد میکند liاگه درون آرایه آیتمی قرار دارد پس به ازای آن یک عنصر todos.forEach(todo => { const li = this.createElement('li') li.id = todo.id const checkbox = this.createElement('input') checkbox.type = 'checkbox' checkbox.checked = todo.complete const span = this.createElement('span') span.contentEditable = true span.classList.add('editable') if (todo.complete) { const strike = this.createElement('s') strike.textContent = todo.text span.append(strike) } else { span.textContent = todo.text } const deleteButton = this.createElement('button', 'delete') deleteButton.textContent = 'Delete' li.append(checkbox, span, deleteButton) // نود ایجاد شده به لیست اضافه میکند this.todoList.append(li) }) } // برای خطایابی و نمایش در کنسول console.log(todos) }
در نهایت قسمت Controller را که پل ارتباطی بین View و Model میباشد، کامل میکنیم. اولین تغییراتی که در کلاس Controller ایجاد میکنیم، استفاده از متد displayTodos در سازندهی این کلاس میباشد و با هر بار تغییر این متد، دوباره فراخوانی میشود:
class Controller { constructor(model, view) { this.model = model this.view = view // نمایش اطلاعات پیش فرض this.onTodoListChanged(this.model.todos) } onTodoListChanged = todos => { this.view.displayTodos(todos) } }
چهار تابعی را که در قسمت Model ایجاد نمودیم و کار ویرایش، حذف، افزودن و اتمام کار را انجام میدادند، در کلاس کنترلر آنها را هندل میکنیم و زمانیکه کاربر دکمهای را برای افزودن یا تیک حذف آیتمی، زد، تابع مربوطه توسط کنترلر در Model فراخوانی شود:
handleAddTodo = todoText => { this.model.addTodo(todoText) } handleEditTodo = (id, todoText) => { this.model.editTodo(id, todoText) } handleDeleteTodo = id => { this.model.deleteTodo(id) } handleToggleTodo = id => { this.model.toggleTodo(id) }
چون کنترلر نمیتواند بصورت مستقیم فراخوانی شود و این توابع باید درون DOM تنظیم شوند تا به ازای رخدادهایی همچون click و change، فراخوانی شوند. پس از این توابع در قسمت View استفاده میکنیم و به کلاس View، موارد زیر را اضافه میکنیم:
bindAddTodo(handler) { this.form.addEventListener('submit', event => { event.preventDefault() if (this._todoText) { handler(this._todoText) this._resetInput() } }) } bindDeleteTodo(handler) { this.todoList.addEventListener('click', event => { if (event.target.className === 'delete') { const id = parseInt(event.target.parentElement.id) handler(id) } }) } bindToggleTodo(handler) { this.todoList.addEventListener('change', event => { if (event.target.type === 'checkbox') { const id = parseInt(event.target.parentElement.id) handler(id) } }) }
برای bind کردن این متدها در کلاس Controller، کدهای زیر را اضافه میکنیم:
this.view.bindAddTodo(this.handleAddTodo) this.view.bindDeleteTodo(this.handleDeleteTodo) this.view.bindToggleTodo(this.handleToggleTodo)
برای ذخیره اطلاعات در local storage، در سازنده کلاس Model، کد زیر را اضافه میکنیم:
this.todos = JSON.parse(localStorage.getItem('todos')) || []
متد دیگری هم در کلاس Model برای بهروز رسانی مقادیر local storage قرار میدهیم:
_commit(todos) { this.onTodoListChanged(todos) localStorage.setItem('todos', JSON.stringify(todos)) }
متدی هم برای تغییراتی که هر زمان بر روی todos اتفاق میافتد، فراخوانی شود:
deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) this._commit(this.todos) }
این مقاله صرفا جهت آشنایی و نمونه کدی از پیاده سازی الگوی MVC در جاوااسکریپت میباشد.
.NET 5 Preview 7 is now available and is ready for evaluation. Here’s what’s new in this release:
- Blazor WebAssembly apps now target .NET 5
- Updated debugging requirements for Blazor WebAssembly
- Blazor accessibility improvements
- Blazor performance improvements
- Certificate authentication performance improvements
- Sending HTTP/2 PING frames
- Support for additional endpoints types in the Kestrel sockets transport
- Custom header decoding in Kestrel
- Other minor improvements
مشکل با Microsoft.Web.Infrastructure
نصب کامپوننت Blazored TextEditor
ابتدا نیاز است بستهی نیوگت آنرا با اجرای دستور زیر، به پروژهی Blazor خود اضافه کرد:
dotnet add package Blazored.TextEditor
libman install quill --provider unpkg --destination wwwroot/lib/quill
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>BlazorServer.App</title> <base href="~/" /> <link href="lib/quill/dist/quill.snow.css" rel="stylesheet" /> <link href="lib/quill/dist/quill.bubble.css" rel="stylesheet" />
<script src="lib/quill/dist/quill.min.js"></script> <script src="_content/Blazored.TextEditor/quill-blot-formatter.min.js"></script> <script src="_content/Blazored.TextEditor/Blazored-BlazorQuill.js"></script> <script src="_framework/blazor.server.js"></script> </body>
و در آخر جهت سهولت کار با این کامپوننت میتوان فضای نام آنرا به فایل BlazorServer.App\_Imports.razor به صورت زیر اضافه کرد:
@using Blazored.TextEditor
استفاده از کامپوننت Blazored.TextEditor در کامپوننت HotelRoomUpsert.razor
میخواهیم در کامپوننت HotelRoomUpsert.razor مثال این سری، بجای کامپوننت InputTextArea مورد استفاده، از یک HTML Editor استفاده کنیم:
<div class="form-group"> <label>Details</label> @*<InputTextArea @bind-Value="HotelRoomModel.Details" class="form-control"></InputTextArea>*@ <BlazoredTextEditor @ref="@QuillHtml"> <ToolbarContent> <select class="ql-header"> <option selected=""></option> <option value="1"></option> <option value="2"></option> <option value="3"></option> <option value="4"></option> <option value="5"></option> </select> <span class="ql-formats"> <button class="ql-bold"></button> <button class="ql-italic"></button> <button class="ql-underline"></button> <button class="ql-strike"></button> </span> <span class="ql-formats"> <select class="ql-color"></select> <select class="ql-background"></select> </span> <span class="ql-formats"> <button class="ql-list" value="ordered"></button> <button class="ql-list" value="bullet"></button> </span> <span class="ql-formats"> <button class="ql-link"></button> </span> </ToolbarContent> <EditorContent> </EditorContent> </BlazoredTextEditor> </div>
- همانطور که ملاحظه میکنید، این تعریف به همراه یک ارجاع به وهلهای از آن نیز هست:
<BlazoredTextEditor @ref="@QuillHtml">
@code { private BlazoredTextEditor QuillHtml;
برای تغییر اندازه و مقدار placeholder پیشفرض آن، میتوان به صورت زیر عمل کرد:
<div class="form-group pb-4" style="height:250px;"> <label>Details</label> <BlazoredTextEditor @ref="@QuillHtml" Placeholder="Please enter the room's detail">
تنظیم و دریافت متن نمایشی HTML Editor
مطابق مستندات این کامپوننت، روش تنظیم متن نمایشی آن، به کمک متد LoadHTMLContent است. به همین جهت متد زیر را به کدهای کامپوننت جاری اضافه میکنیم:
private async Task SetHTMLAsync() { if(!string.IsNullOrEmpty(HotelRoomModel.Details)) { await QuillHtml.LoadHTMLContent(HotelRoomModel.Details); } }
protected override async Task OnInitializedAsync() { if (Id.HasValue) { // Update Mode Title = "Update"; HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value); await SetHTMLAsync(); } // ... }
private async Task HandleHotelRoomUpsert() { // ... // Create Mode HotelRoomModel.Details = await QuillHtml.GetHTML(); // ... }
مشکل! ادیتور در زمان ویرایش یک رکورد، اطلاعات پیشین را نمایش نمیدهد!
پس از اعمال تغییرات فوق، برنامه را اجرا میکنیم. سپس یک اتاق جدید را اضافه کرده و در لیست نمایش اتاقها، گزینهی ویرایش آنرا انتخاب میکنیم. در این حالت هرچند کار مقدار دهی HotelRoomModel.Details در زمان ثبت اطلاعات انجام شده، اما ... در زمان ویرایش چیزی نمایش داده نمیشود و تغییراتی را که به متد رویدادگردان OnInitializedAsync اضافه کردهایم، عمل نمیکنند.
در این مورد در قسمت بررسی چرخهی حیات کامپوننتها توضیحاتی ابتدایی ارائه شد:
«رویدادهای OnAfterRender و OnAfterRenderAsync
پس از هر بار رندر کامپوننت، این متدها فراخوانی میشوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آنها به پایان رسیدهاست. یکی از کاربردهای آن، آغاز کامپوننتهای جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.»
بنابراین در این حالت خاص که ادیتور جاوا اسکریپتی مورد استفاده، پس از رندر کامل UI نمایش داده میشود، قرار دادن متد SetHTML در روال رویدادگردان OnInitializedAsync کار نخواهد کرد و باید آنرا به روال رویدادگردان OnAfterRender انتقال دهیم:
protected override async Task OnAfterRenderAsync(bool firstRender) { await SetHTMLAsync(); }
private async Task SetHTMLAsync() { if(!string.IsNullOrEmpty(HotelRoomModel.Details)) { await QuillHtml.LoadHTMLContent(HotelRoomModel.Details); StateHasChanged(); } }
مشکل! اگر در این حالت سعی کنیم متنی را در ادیتور وارد کنیم، میسر نیست و همچنین CPU Usage سیستم به 100 درصد رسیدهاست!
علت اینجا است که فراخوانی StateHasChanged، هر چند سبب رندر مجدد UI میشود، اما چون در پایان کار رندر قرار داریم، یک حلقهی بینهایت را سبب خواهد شد. به همین جهت باید در متد OnAfterRenderAsync، بر اساس پارامتر firstRender، از رندرهای بعدی جلوگیری کرد:
protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) { return; } while (true) { try { await SetHTMLAsync(); break; } catch { await Task.Delay(100); // Quill needs some time to load } } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-20.zip