البرنامج التعليمي الكامل للمبتدئين بخطوة لبناء قائمة TODO في فينيكس.
100 ٪ وظيفية. 0 ٪ JavaScript. فقط HTML ، CSS و Elixir . سريع وقابل للصيانة.
قوائم TODO مألوفة لمعظم الناس. نقدم قوائم طوال الوقت. يعد بناء قائمة TODO من نقطة الصفر طريقة رائعة لتعلم Elixir / Phoenix لأن واجهة المستخدم / UX بسيطة ، حتى نتمكن من التركيز على التنفيذ.
بالنسبة للفريق @dwyl هذا التطبيق/البرنامج التعليمي يعد عرضًا لكيفية تقديم جانب الخادم ( مع تعزيز التدريجي من جانب العميل ) يمكن أن يوفر توازنًا ممتازًا بين فعالية المطور ( ميزات الشحن بسرعة ) و UX وإمكانية الوصول . تستغرق الصفحات المقدمة من الخادم أقل من 5 مللي ثانية للاستجابة بحيث تكون UX سريعة . On Fly.io: Phxtodo.fly.dev أوقات الاستجابة للرحمة هي 200 مللي ثانية لجميع التفاعلات ، لذلك يبدو وكأنه تطبيق من جانب العميل المقدم.
برنامج TODO LIST الذي يعرض مبتدئًا كاملاً كيفية إنشاء تطبيق في Elixir/Phoenix من نقطة الصفر.
جرب نسخة fly.io. أضف بضع عناصر إلى القائمة واختبار الوظيفة.

حتى مع الرحلة المستديرة HTTP الكاملة لكل تفاعل ، فإن وقت الاستجابة سريع . انتبه إلى كيفية Chrome | Firefox | Safari ينتظر الاستجابة من الخادم قبل إعادة تقديم الصفحة. ذهب تحديث الصفحة الكاملة القديمة من الأمس. المتصفحات الحديثة تقدم بذكاء التغييرات فقط! لذلك يقارب UX "الأصلي"! على محمل الجد ، جرب تطبيق Fly.io على هاتفك وانظر!
في هذا البرنامج التعليمي ، نستخدم TODODVC CSS لتبسيط واجهة المستخدم الخاصة بنا. هذا له العديد من المزايا الأكبر هو تقليل مقدار CSS الذي يجب أن نكتبه! وهذا يعني أيضًا أن لدينا دليلًا يجب تنفيذ الميزات لتحقيق وظائف كاملة.
ملاحظة : نحن نحب
CSSلقوته/مرونته المذهلة ، لكننا نعلم أنه ليس الجميع يحبون ذلك. انظر: Learn-Tachyons#لماذا آخر شيء نريده هو إهدار الكثير من الوقت معCSSفي برنامج تعليميPhoenix!
هذا البرنامج التعليمي هو لأي شخص يتعلم إكسير/فينيكس. لا يُفترض/متوقع أي خبرة سابقة مع فينيكس. لقد قمنا بتضمين جميع الخطوات المطلوبة لإنشاء التطبيق.
إذا تعثرت في أي خطوة ، فيرجى فتح مشكلة على Github حيث يسعدنا مساعدتك في التغلب على! إذا شعرت أن أي سطر من التعليمات البرمجية يمكنه استخدام المزيد من التفسير/الوضوح ، فالرجاء عدم التردد في إبلاغنا ! نحن نعرف ما يشبه أن تكون مبتدئًا ، فقد يكون الأمر محبطًا عندما لا يكون هناك شيء منطقي! إن طرح أسئلة على Github يساعد الجميع على التعلم!
من فضلك تعطينا ملاحظات! repo star إذا وجدت أنه مفيد.
قبل محاولة إنشاء قائمة TODO ، تأكد من أن لديك كل ما تحتاجه مثبتة على جهاز الكمبيوتر الخاص بك. انظر: المتطلبات الأساسية
بمجرد أن تؤكد أن لديك Phoenix & Postgresql مثبتة ، حاول تشغيل التطبيق النهائي .
localhost قبل البدء في إنشاء نسختك الخاصة من تطبيق Todo List ، قم بتشغيل الإصدار النهائي على localhost لتأكيد أنه يعمل.
استنساخ المشروع من جيثب:
git clone [email protected]:dwyl/phoenix-todo-list-tutorial.git && cd phoenix-todo-list-tutorialتثبيت التبعيات وإعداد قاعدة البيانات:
mix setupابدأ خادم Phoenix:
mix phx.server قم بزيارة localhost:4000 في متصفح الويب الخاص بك.
يجب أن ترى:

الآن بعد أن أصبح لديك تطبيق المثال النهائي يعمل على مضيفك localhost ،
دعنا نبنيها من الصفر ونفهم كل الخطوات.
عند تشغيل تطبيق المثال النهائي على localhost ، إذا كنت تريد تجربة زر login ، فستحتاج إلى الحصول على AUTH_API_KEY . [1 دقيقة] انظر: AUTH_API_KEY
إذا قمت بتشغيل التطبيق النهائي على مضيفك localhost ( ويجب عليك حقًا! ) ،
ستحتاج إلى تغيير دليل قبل بدء البرنامج التعليمي:
cd ..
أنت الآن مستعد للبناء !
في المحطة الخاصة بك ، قم بإنشاء تطبيق Phoenix جديد باستخدام أمر mix التالي:
mix phx.new app --no-dashboard --no-gettext --no-mailer عند المطالبة بتثبيت التبعيات ، اكتب Y متبوعًا بإدخال .
ملاحظة : هذه الأعلام بعد اسم
appهي فقط لتجنب إنشاء الملفات التي لا نحتاجها لهذا المثال البسيط. انظر: Hexdocs.pm/phoenix/mix.tasks.phx.new
التغيير إلى دليل app الذي تم إنشاؤه حديثًا ( cd app ) وتأكد من أن لديك كل ما تحتاجه:
mix setupابدأ خادم Phoenix:
mix phx.server يمكنك الآن زيارة localhost:4000 في متصفح الويب الخاص بك. يجب أن ترى شيئًا مشابهًا لـ:

أغلق خادم Phoenix Ctrl + C.
قم بإجراء اختبارات لضمان عمل كل شيء كما هو متوقع:
mix testيجب أن ترى:
Compiling 16 files (.ex)
Generated app app
17:49:40.111 [info] Already up
...
Finished in 0.04 seconds
3 tests, 0 failuresبعد أن أثبتت أن تطبيق Phoenix يعمل كما هو متوقع ، دعنا ننتقل إلى إنشاء بعض الملفات!
items في إنشاء قائمة TODO الأساسية ، نحتاج فقط إلى مخطط واحد: items . في وقت لاحق ، يمكننا إضافة قوائم وعلامات منفصلة لتنظيم/تصنيف items ولكن هذا هو الآن كل ما نحتاجه.
قم بتشغيل أمر المولد التالي لإنشاء جدول العناصر:
mix phx.gen.html Todo Item items text:string person_id:integer status:integer بالمعنى الدقيق للكلمة ، نحن بحاجة فقط إلى حقول text status ، ولكن نظرًا لأننا نعلم أننا نريد ربط العناصر بالأشخاص (_later في البرنامج التعليمي) ، فإننا نضيف الحقل الآن .
سترى الإخراج التالي:
* creating lib/app_web/controllers/item_controller.ex
* creating lib/app_web/controllers/item_html/edit.html.heex
* creating lib/app_web/controllers/item_html/index.html.heex
* creating lib/app_web/controllers/item_html/new.html.heex
* creating lib/app_web/controllers/item_html/show.html.heex
* creating lib/app_web/controllers/item_html.ex
* creating test/app_web/controllers/item_controller_test.exs
* creating lib/app/todo/item.ex
* creating priv/repo/migrations/20221205102303_create_items.exs
* creating lib/app/todo.ex
* injecting lib/app/todo.ex
* creating test/app/todo_test.exs
* injecting test/app/todo_test.exs
* creating test/support/fixtures/todo_fixtures.ex
* injecting test/support/fixtures/todo_fixtures.ex
Add the resource to your browser scope in lib/app_web/router.ex:
resources "/items", ItemController
Remember to update your repository by running migrations:
$ mix ecto.migrate
التي خلقت مجموعة من الملفات! بعضها لا نحتاجه بدقة.
يمكننا إنشاء الملفات التي نحتاجها يدويًا ، ولكن هذه هي الطريقة "الرسمية" لإنشاء تطبيق CRUD في Phoenix ، لذلك نحن نستخدمها للسرعة.
ملاحظة : سياقات Phoenix التي يتم الإشارة إليها في هذا المثال كـ
Todo، هي " وحدات مخصصة تعرض الوظائف المتعلقة بالمجموعة ." نشعر أنهم يعقدون بشكل غير ضروري تطبيقات Phoenix الأساسية مع طبقات من "الواجهة" ونتمنى حقًا أن نتمكن من تجنبها. ولكن بالنظر إلى أنهم يخبزون في المولدات ، ويعجبهم منشئ الإطار ، فلدينا خيار: إما أن ندخل في السياقات أو إنشاء جميع الملفات يدويًا في مشاريع فينيكس لدينا. المولدات هي طريقة أسرع بكثير للبناء! احتضنهم ، حتى لو انتهى بك الأمر إلىdeleteبعض الملفات غير المستخدمة على طول الطريق!
لن نشرح كل من هذه الملفات في هذه المرحلة في البرنامج التعليمي لأنه من الأسهل فهم الملفات أثناء قيامك بإنشاء التطبيق! سيصبح الغرض من كل ملف واضحًا مع تقدمك من خلال تحريرها.
/items إلى router.ex اتبع الإرشادات التي لاحظها المولد لإضافة resources "/items", ItemController إلى router.ex .
افتح ملف lib/app_web/router.ex وتحديد موقع السطر: scope "/", AppWeb do . أضف الخط إلى نهاية الكتلة. على سبيل المثال:
scope "/" , AppWeb do
pipe_through :browser
get "/" , PageController , :index
resources "/items" , ItemController # this is the new line
end يجب أن يبدو ملف router.ex الخاص بك مثل هذا: router.ex#L20
الآن ، كما اقترح المحطة ، قم بتشغيل mix ecto.migrate . سيؤدي ذلك إلى إنشاء جداول قاعدة البيانات وتشغيل الترحيل اللازمة حتى يعمل كل شيء بشكل صحيح!
في هذه المرحلة ، لدينا بالفعل قائمة TODO الوظيفية ( إذا كنا على استعداد لاستخدام PHOENIX UI ).
حاول تشغيل التطبيق على localhost : قم بتشغيل الترحيل الذي تم إنشاؤه باستخدام mix ecto.migrate ثم الخادم مع:
mix phx.server
تفضل بزيارة: http: // localhost: 4000/stem/new ودخل بعض البيانات.

انقر فوق الزر "حفظ العنصر" وسيتم إعادة توجيهك إلى الصفحة "show": http: // localhost: 4000/1 stems

هذه ليست تجربة مستخدم جذابة (UX) ، لكنها تعمل ! فيما يلي قائمة بالعناصر - "قائمة TODO". يمكنك زيارة هذا بالنقر فوق الزر " Back to items أو عن طريق الوصول إلى عنوان URL التالي HTTP: // LocalHost: 4000/عنصر.

دعونا نحسن UX باستخدام TODODVC HTML و CSS !
لإعادة إنشاء TODODVC UI/UX ، دعنا نقترض رمز HTML مباشرة من المثال.
تفضل بزيارة: http://todomvc.com/examples/vanillajs أضف بعض العناصر إلى القائمة. بعد ذلك ، افحص المصدر باستخدام أدوات DEV الخاصة بالمتصفح. على سبيل المثال:

انقر بزر الماوس الأيمن على المصدر الذي تريده (على سبيل المثال: <section class="todoapp"> ) وحدد "تحرير كـ HTML":

بمجرد أن تكون HTML لـ <section> قابلة للتحرير ، حددها ونسخها.

رمز HTML هو:
< section class =" todoapp " >
< header class =" header " >
< h1 > todos </ h1 >
< input class =" new-todo " placeholder =" What needs to be done? " autofocus ="" />
</ header >
< section class =" main " style =" display: block; " >
< input id =" toggle-all " class =" toggle-all " type =" checkbox " />
< label for =" toggle-all " > Mark all as complete </ label >
< ul class =" todo-list " >
< li data-id =" 1590167947253 " class ="" >
< div class =" view " >
< input class =" toggle " type =" checkbox " />
< label > Learn how to build a Todo list in Phoenix </ label >
< button class =" destroy " > </ button >
</ div >
</ li >
< li data-id =" 1590167956628 " class =" completed " >
< div class =" view " >
< input class =" toggle " type =" checkbox " />
< label > Completed item </ label >
< button class =" destroy " > </ button >
</ div >
</ li >
</ ul >
</ section >
< footer class =" footer " style =" display: block; " >
< span class =" todo-count " > < strong > 1 </ strong > item left </ span >
< ul class =" filters " >
< li >
< a href =" #/ " class =" selected " > All </ a >
</ li >
< li >
< a href =" #/active " > Active </ a >
</ li >
< li >
< a href =" #/completed " > Completed </ a >
</ li >
</ ul >
< button class =" clear-completed " style =" display: block; " >
Clear completed
</ button >
</ footer >
</ section > دعنا نحول هذا HTML إلى قالب ELIXIR ( EEx ) المضمن.
ملاحظة : السبب في أننا نقوم بنسخ
HTMLهذا من مفتش عناصر المستعرض بدلاً من المصدر مباشرة على Github:examples/vanillajs/index.htmlهو أن هذا "تطبيق صفحة واحدة" ، وبالتالي فإن<ul class="todo-list"></ul>يحصل فقط على السكان في التطهير. يعد نسخها من أدوات Dev Browser أسهل طريقة للحصول علىHTMLالكامل .
index.html.eex افتح ملف lib/app_web/controllers/item_html/index.html.eex وتمريره إلى الأسفل.
ثم ( دون إزالة الكود الموجود بالفعل ) ، قم بصق رمز HTML الذي حصلنا عليه من TODODVC.
على سبيل المثال:
/lib/app_web/controllers/item_html/index.html.eex#L27-L73
إذا حاولت تشغيل التطبيق الآن وزيارة http: // localhost: 4000/stem/
سترى هذا ( بدون TODODVC CSS ):

من الواضح أن هذا ليس ما نريده ، لذلك دعونا نحصل على TODODVC CSS وننقذها في مشروعنا!
/assets/css تفضل بزيارة http://todomvc.com/examples/vanillajs/node_modules/todomvc-app-css/index.css
وحفظ الملف إلى /assets/css/todomvc-app.css todomvc-app.css.
على سبيل المثال: /assets/css/todomvc-app.css
todomvc-app.css في app.scss افتح ملف assets/css/app.scss واستبدله بما يلي:
/* This file is for your main application css. */
/* @import "./phoenix.css"; */
@import "./todomvc-app.css" ; على سبيل المثال: /assets/css/app.scss#L4
افتح ملف lib/app_web/components/layouts/app.html.heex واستبدل المحتويات بالرمز التالي:
<!DOCTYPE html >
< html lang =" en " >
< head >
< meta charset =" utf-8 " />
< meta http-equiv =" X-UA-Compatible " content =" IE=edge " />
< meta name =" viewport " content =" width=device-width, initial-scale=1.0 " />
< title > Phoenix Todo List </ title >
< link rel =" stylesheet " href = {~p "/assets/app.css"} />
< script defer type =" text/javascript " src = {~p "/assets/app.js"} > </ script >
</ head >
< body >
< main role =" main " class =" container " >
< %= @inner_content % >
</ main >
</ body >
</ html >قبل:
lib/app_web/components/layouts/app.html.eex
بعد:lib/app_web/components/layouts/app.html.heex
<%= @inner_content %> هو المكان الذي سيتم فيه تقديم تطبيق TODO.
ملاحظة : يتم تضمين علامة
<script>خارج الاتفاقية. ومع ذلك ، لن نكتب أيJavaScriptفي هذا البرنامج التعليمي. سنحقق 100 ٪ من التكافؤ مع TODODVC ، دون كتابة خط منJS. نحن لا "نكره"JS، في الواقع لدينا برنامج تعليمي "أخت" يبني نفس التطبيق فيJS: Dwyl/JavaScript-Todo-Listorial-نريد فقط أن نذكرك بأنك لا تحتاج إلى أيJSلإنشاء تطبيق ويب يعمل بكامل طاقته مع UX رائع!
مع حفظ قالب التخطيط ، تم حفظ ملف TODODVC CSS على /assets/css/todomvc-app.css و todomvc-app.css في app.scss ، يجب الآن أن تبدو صفحة /items الخاصة بك مثل هذا:

لذلك بدأت قائمة TODO الخاصة بنا تبدو مثل TODOMVC ، لكنها لا تزال مجرد قائمة وهمية.
من أجل تقديم بيانات item في قالب TODODVC ، سنحتاج إلى إضافة بعض الوظائف. عندما أنشأنا المشروع وقمنا بإنشاء نموذج item ، تم إنشاء وحدة تحكم (موجودة في lib/app_web/controllers/item_controller.ex ) ومكون/عرض أيضًا (موجود في lib/app_web/controllers/item_html.ex ). هذا المكون/العرض هو ما يتحكم بشكل فعال في عرض المحتويات داخل دليل lib/app_web/controllers/item_html الذي تراجعنا عنه مسبقًا.
نحن نعلم أننا بحاجة إلى إجراء تغييرات على واجهة المستخدم ، لذلك سنضيف بعض الوظائف في هذا المكون (وهو أقرب إلى جزء العرض من نموذج MVC).
هذه هي فرصتنا الأولى للقيام ببعض التطوير الذي يحركه الاختبار (TDD).
قم بإنشاء ملف جديد مع test/app_web/controllers/item_html_test.exs .
اكتب الرمز التالي في الملف:
defmodule AppWeb.ItemHTMLTest do
use AppWeb.ConnCase , async: true
alias AppWeb.ItemHTML
test "complete/1 returns completed if item.status == 1" do
assert ItemHTML . complete ( % { status: 1 } ) == "completed"
end
test "complete/1 returns empty string if item.status == 0" do
assert ItemHTML . complete ( % { status: 0 } ) == ""
end
end على سبيل المثال: /test/app_web/controllers/item_html_test.exs
إذا حاولت تشغيل ملف الاختبار هذا:
mix test test/app_web/controllers/item_html_test.exsسترى الخطأ التالي (لأن الوظيفة غير موجودة بعد!):
** (UndefinedFunctionError) function AppWeb.ItemHTML.checked/1 is undefined or private
افتح ملف lib/app_web/controllers/item_html.ex واكتب الوظائف لجعل الاختبارات تمر .
هذه هي الطريقة التي قمنا بتنفيذ الوظائف. يجب الآن أن يبدو ملف item_html.ex الخاص بك مثل ما يلي.
defmodule AppWeb.ItemHTML do
use AppWeb , :html
embed_templates "item_html/*"
# add class "completed" to a list item if item.status=1
def complete ( item ) do
case item . status do
1 -> "completed"
_ -> "" # empty string means empty class so no style applied
end
end
endأعد تشغيل الاختبارات ويجب أن تمر الآن:
mix test test/app_web/controllers/item_html_test.exsيجب أن ترى:
....
Finished in 0.1 seconds
4 tests, 0 failuresالآن وقد قمنا بإنشاء هاتين العرضين ، واختباراتنا تمر ، دعنا نستخدمها في قالبنا!
افتح ملف lib/app_web/controllers/item_html/index.html.eex وتحديد موقع السطر:
< ul class =" todo-list " > استبدل محتويات <ul> بما يلي:
< %= for item < - @items do % >
< li data-id = {item.id} class = {complete(item)} >
< div class =" view " >
< %= if item.status == 1 do % >
< input class =" toggle " type =" checkbox " checked />
< % else % >
< input class =" toggle " type =" checkbox " />
< % end % >
< label > < %= item.text % > </ label >
< .link
class="destroy"
href={~p"/items/#{item}"}
method="delete"
>
</ .link >
</ div >
</ li >
< % end % > على سبيل المثال: lib/app_web/controllers/item_html/index.html.heex#L43-L53
مع حفظ هذين الملفان ، إذا قمت بتشغيل التطبيق الآن: mix phx.server وزيارة http: // localhost: 4000/stem.
سترى items الحقيقية التي أنشأتها في الخطوة 2.2 أعلاه:

الآن بعد أن أصبح لدينا عناصرنا التي يتم عرضها في تصميم TODODVC ، دعنا نعمل على إنشاء عناصر جديدة في نمط "تطبيق الصفحة الفردية".
في الوقت الحاضر ، يتوفر نموذج "العنصر الجديد" على: http: // localhost: 4000/new ( كما هو مذكور في الخطوة 2 أعلاه )
نريد أن يكون الشخص قادرًا على إنشاء عنصر جديد دون الحاجة إلى الانتقال إلى صفحة مختلفة. من أجل تحقيق هذا الهدف ، سنقوم بتضمين قالب lib/app_web/controllers/item_html/new.html.heex ( جزئي ) داخل قالب lib/app_web/controllers/item_html/index.html.heex . على سبيل المثال:
قبل أن نتمكن من القيام بذلك ، نحتاج إلى ترتيب قالب new.html.heex لإزالة الحقول التي لا نحتاجها .
دعنا نفتح lib/app_web/controllers/item_html/new.html.heex وتبسيطه فقط إلى الحقل الأساسي :text :
< . simple_form :let = { f } for = { @ changeset } action = { ~p " /items " } >
< . input
field = { { f , :text } }
type = "text"
placeholder = "what needs to be done?"
/ >
< :actions >
< . button style = "display:none" > Save Item< / . button >
< / :actions >
< / . simple_form >
قبل:
/lib/app_web/controllers/item_html/new.html.heex
بعد:/lib/app_web/controllers/item_html/new.html.heex
نحن بحاجة إلى تغيير نمط علامة <.input> . مع Phoenix ، داخل ملف lib/app_web/components/core_components.ex ، يتم تعريف الأنماط للمكونات التي تم إنشاؤها مسبقًا (وهذا هو الحال مع <.input> ).
لتغيير هذا بحيث يستخدم نفس النمط مثل TODOMVC ، حدد موقع السطر التالي.
def input ( assigns ) do
قم بتغيير سمة الفصل مع فئة new-todo . يجب أن تبدو هذه الوظيفة مثل ما يلي.
def input ( assigns ) do
~H """
<div phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<input
type={@type}
name={@name}
id={@id || @name}
value={@value}
class={[
input_border(@errors),
"new-todo"
]}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end نحتاج أيضًا إلى تغيير أنماط actions داخل simple_form . في نفس الملف ، ابحث عن def simple_form(assigns) do بتغييره حتى يبدو الأمر كذلك:
def simple_form ( assigns ) do
~H """
<.form :let={f} for={@for} as={@as} {@rest}>
<div>
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions}>
<%= render_slot(action, f) %>
</div>
</div>
</.form>
"""
end إذا قمت بتشغيل تطبيق Phoenix الآن وزيارة http: // localhost: 4000/stem/new ، ستشاهد الحقل الإدخال المنفرد :text لا يوجد زر "حفظ":

لا تقلق ، لا يزال بإمكانك إرسال النموذج باستخدام مفتاح Enter (Return). ومع ذلك ، إذا حاولت إرسال النموذج الآن ، فلن يعمل ذلك لأننا أزلنا اثنين من الحقول المطلوبة من قبل changeset ! دعونا نصلح ذلك.
items لتعيين القيم default بالنظر إلى أننا أزلنا اثنين من الحقول ( :person_id و :status ) من new.html.eex ، نحتاج إلى التأكد من وجود قيم افتراضية لهذه في المخطط. افتح ملف lib/app/todo/item.ex واستبدل المحتويات بما يلي:
defmodule App.Todo.Item do
use Ecto.Schema
import Ecto.Changeset
schema "items" do
field :person_id , :integer , default: 0
field :status , :integer , default: 0
field :text , :string
timestamps ( )
end
@ doc false
def changeset ( item , attrs ) do
item
|> cast ( attrs , [ :text , :person_id , :status ] )
|> validate_required ( [ :text ] )
end
end نحن هنا نقوم بتحديث schema "العناصر" لتعيين القيمة الافتراضية لـ 0 لكل من person_id و status . وفي changeset/2 نقوم بإزالة متطلبات person_id status . وبهذه الطريقة ، يمكن تقديم نموذج item الجديد الخاص بنا باستخدام حقل text فقط .
على سبيل المثال: /lib/app/todo/item.ex#L6-L7
الآن وبعد أن أصبح لدينا قيم default لـ person_id status إذا قمت بإرسال /items/new ، فسوف ينجح.
index/2 في ItemController من أجل وضع نموذج العنصر الجديد ( new.html.eex ) في قالب index.html.eex ، نحتاج إلى تحديث AppWeb.ItemController.index/2 لتضمين تغيير.
افتح ملف lib/app_web/controllers/item_controller.ex وقم بتحديث وظيفة index/2 إلى ما يلي:
def index ( conn , _params ) do
items = Todo . list_items ( )
changeset = Todo . change_item ( % Item { } )
render ( conn , "index.html" , items: items , changeset: changeset )
end قبل: /lib/app_web/controllers/item_controller.ex
بعد: /lib/app_web/controllers/item_controller.ex#L9-L10
لن ترى أي تغيير في واجهة المستخدم أو الاختبارات بعد هذه الخطوة. ما عليك سوى الانتقال إلى 5.3 حيث تحدث لحظة "آها".
new.html.eex داخل index.html.eex الآن وبعد أن قمنا بكل أعمال التحضير ، فإن الخطوة التالية هي تقديم قالب new.html.eex ( جزئي ) داخل قالب index.html.eex .
افتح ملف lib/app_web/controllers/item_html/index.html.heex وتحديد موقع السطر:
< input class =" new-todo " placeholder =" What needs to be done? " autofocus ="" >استبدله بهذا:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % > دعونا نتفكك ما فعلناه للتو. نقوم بتضمين new.html.heex جزئي داخل ملف index.html.heex . نحن نقوم بذلك عن طريق الاتصال بالوظيفة new/2 داخل item_controller.ex . تتعلق هذه الوظيفة بالصفحة الموجودة في items/new وتقوم بإعداد ملف new.html.heex . وبالتالي لماذا نسمي هذه الوظيفة للتضمين بنجاح؟
قبل: /lib/app_web/controllers/item_html/index.html.heex#L36
بعد: /lib/app_web/controllers/item_html/index.html.heex#L36
إذا قمت بتشغيل التطبيق الآن وزيارة: http: // localhost: 4000/stem
يمكنك إنشاء عنصر عن طريق كتابة النص الخاص بك وإرساله باستخدام مفتاح Enter (Return).

إعادة التوجيه إلى قالب "show" هو "موافق" ، ولكن يمكننا القيام بعمل أفضل من خلال إعادة توجيه إلى العودة إلى قالب index.html . لحسن الحظ ، هذا سهل مثل تحديث سطر واحد في الرمز.
redirect في create/2 افتح ملف lib/app_web/controllers/item_controller.ex وتحديد وظيفة create . على وجه التحديد الخط:
|> redirect ( to: ~p " /items/ #{ item } " )قم بتحديث الخط إلى:
|> redirect ( to: ~p " /items/ " ) قبل: /lib/app_web/controllers/item_controller.ex#L22
بعد: /lib/app_web/controllers/item_controller.ex#L23
الآن عندما نقوم بإنشاء item جديد ، يتم إعادة توجيهنا إلى قالب index.html :

item_controller_test.exs لإعادة التوجيه إلى index التغييرات التي أجريناها على ملفات new.html.heex والخطوات المذكورة أعلاه قد كسرت بعض الاختبارات الآلية لدينا. يجب أن نصلح ذلك.
قم بإجراء الاختبارات:
mix testسترى الإخراج التالي:
Finished in 0.08 seconds (0.03s async, 0.05s sync)
23 tests, 3 failures
افتح test/app_web/controllers/item_controller_test.exs وتحديد موقعه describe "new item" describe "create item" . تغيير هذين إلى ما يلي.
استبدل الاختبار:
describe "new item" do
test "renders form" , % { conn: conn } do
conn = get ( conn , ~p " /items/new " )
assert html_response ( conn , 200 ) =~ "what needs to be done?"
end
end
describe "create item" do
test "redirects to show when data is valid" , % { conn: conn } do
conn = post ( conn , ~p " /items " , item: @ create_attrs )
assert % { } = redirected_params ( conn )
assert redirected_to ( conn ) == ~p " /items/ "
end
test "errors when invalid attributes are passed" , % { conn: conn } do
conn = post ( conn , ~p " /items " , item: @ invalid_attrs )
assert html_response ( conn , 200 ) =~ "can't be blank"
end
endرمز محدث:
/test/app_web/controllers/item_controller_test.exs#L34-L55
إذا قمت بإعادة تشغيل اختبارات mix test فلا تمر جميعها الآن.
......................
Finished in 0.2 seconds (0.09s async, 0.1s sync)
22 tests, 0 failuresحتى الآن تعمل الوظيفة الرئيسية لـ TODODVC UI ، ويمكننا إنشاء عناصر جديدة وتظهر في قائمتنا. في هذه الخطوة ، سنعزز واجهة المستخدم لتضمين عدد العناصر المتبقية في الزاوية اليسرى السفلية.
افتح test/app_web/controllers/item_html_test.exs وإنشاء الاختبارين التاليين:
test "remaining_items/1 returns count of items where item.status==0" do
items = [
% { text: "one" , status: 0 } ,
% { text: "two" , status: 0 } ,
% { text: "done" , status: 1 }
]
assert ItemHTML . remaining_items ( items ) == 2
end
test "remaining_items/1 returns 0 (zero) when no items are status==0" do
items = [ ]
assert ItemHTML . remaining_items ( items ) == 0
end على سبيل المثال: test/app_web/controllers/item_html_test.exs#L14-L26
ستفشل هذه الاختبارات لأن وظيفة ItemHTML.remaining_items/1 غير موجودة.
قم بإجراء تمريرات الاختبارات عن طريق إضافة الكود التالي إلى ملف lib/app_web/controllers/item_html.ex :
# returns integer value of items where item.status == 0 (not "done")
def remaining_items ( items ) do
Enum . filter ( items , fn i -> i . status == 0 end ) |> Enum . count
end على سبيل المثال: /lib/app_web/controllers/item_html#L15-L17
الآن وبعد أن تمر الاختبارات ، استخدم remaining_items/1 في قالب index.html . افتح ملف lib/app_web/controllers/item_html/index.html.eex وتحديد سطر الكود:
< span class =" todo-count " > < strong > 1 </ strong > item left </ span >استبدله بهذا السطر:
< span class =" todo-count " > < %= remaining_items(@items) % > items left </ span > هذا فقط يستدعي وظيفة ItemHTML.remaining_items/1 مع قائمة @items التي ستعود عدد عدد صحيح للعناصر المتبقية التي لم يتم بعد ".
على سبيل المثال: /lib/app_web/controllers/item_html/index.html.eex#L60
عند هذه النقطة ، تعمل العناصر (المتبقية) في أسفل اليسار من واجهة المستخدم TODODVC!
أضف عنصرًا new إلى قائمتك وشاهد زيادة عدد:

كان ذلك سهلاً بما فيه الكفاية ، دعنا نجرب شيئًا أكثر تقدماً!
خذ قسطًا من الراحة والاستيلاء على كوب من الماء الطازج ، سيكون القسم التالي مكثفًا !
status عنصر todo إلى 1 واحدة من الوظائف الأساسية لقائمة TODO هي تبديل status item من 0 إلى 1 ("كاملة").
في المخطط لدينا item مكتمل لديه status 1 .
سنحتاج إلى وظيفتين في وحدة التحكم لدينا:
toggle_status/1 يبطح حالة عنصر على سبيل المثال: 0 إلى 1 و 1 إلى 0.toggle/2 وظيفة المعالج لطلبات HTTP لتبديل حالة عنصر ما. افتح test/app_web/controllers/item_controller_test.exs . سنقوم بإجراء بعض التغييرات هنا حتى نتمكن من إضافة اختبارات إلى الوظائف التي ذكرناها مسبقًا. سنقوم باستيراد App.Todo داخل item_controller_test.exs وإصلاح إنشاء الثوابت وتجنبها لإنشاء عناصر وهمية. تأكد من أن بداية الملف تبدو كذلك.
defmodule AppWeb.ItemControllerTest do
use AppWeb.ConnCase
alias App.Todo
import App.TodoFixtures
@ create_attrs % { person_id: 42 , status: 0 , text: "some text" }
@ public_create_attrs % { person_id: 0 , status: 0 , text: "some public text" }
@ completed_attrs % { person_id: 42 , status: 1 , text: "some text completed" }
@ public_completed_attrs % { person_id: 0 , status: 1 , text: "some public text completed" }
@ update_attrs % { person_id: 43 , status: 1 , text: "some updated text" }
@ invalid_attrs % { person_id: nil , status: nil , text: nil }
نحن نضيف سمات Item الثابتة لاستخدامها لاحقًا في الاختبارات. نحن نحدد Item public لأننا سنضيف لاحقًا مصادقة إلى هذا التطبيق.
بعد ذلك ، حدد موقع defp create_item()/1 داخل نفس الملف. تغييره بحيث يبدو ذلك.
defp create_item ( _ ) do
item = item_fixture ( @ create_attrs )
% { item: item }
end سنستخدم هذه الوظيفة لإنشاء كائنات Item لاستخدامها في الاختبارات التي سنضيفها. الحديث عن ذلك ، دعونا نفعل ذلك! أضف المقتطف التالي إلى الملف.
describe "toggle updates the status of an item 0 > 1 | 1 > 0" do
setup [ :create_item ]
test "toggle_status/1 item.status 1 > 0" , % { item: item } do
assert item . status == 0
# first toggle
toggled_item = % { item | status: AppWeb.ItemController . toggle_status ( item ) }
assert toggled_item . status == 1
# second toggle sets status back to 0
assert AppWeb.ItemController . toggle_status ( toggled_item ) == 0
end
test "toggle/2 updates an item.status 0 > 1" , % { conn: conn , item: item } do
assert item . status == 0
get ( conn , ~p ' /items/toggle/ #{ item . id } ' )
toggled_item = Todo . get_item! ( item . id )
assert toggled_item . status == 1
end
end على سبيل المثال: /test/app_web/controllers/item_controller_test.exs#L64-L82
افتح ملف lib/app_web/controllers/item_controller.ex وأضف الوظائف التالية إليه:
def toggle_status ( item ) do
case item . status do
1 -> 0
0 -> 1
end
end
def toggle ( conn , % { "id" => id } ) do
item = Todo . get_item! ( id )
Todo . update_item ( item , % { status: toggle_status ( item ) } )
conn
|> redirect ( to: ~p " /items " )
end على سبيل المثال: /lib/app_web/controllers/item_controller.ex#L64-L76
ستظل الاختبارات تفشل في هذه المرحلة لأن المسار الذي ندعوه في اختبارنا لم يكن موجودًا بعد. دعونا نصلح ذلك!
get /items/toggle/:id المسار الذي يستدعي toggle/2 افتح lib/app_web/router.ex وتحديد resources "/items", ItemController . أضف خطًا جديدًا:
get "/items/toggle/:id" , ItemController , :toggle على سبيل المثال: /lib/app_web/router.ex#L21
الآن ستمر اختباراتنا أخيرًا :
mix testيجب أن ترى:
22:39:42.231 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failurestoggle/2 عند النقر فوق مربع الاختيار في index.html الآن وبعد أن تمر اختباراتنا ، فقد حان الوقت لاستخدام كل هذه الوظائف التي نبنيها في واجهة المستخدم. افتح /lib/app_web/controllers/item_html/index.html.heex controllers/item_html/index.html.heex ملف وتحديد موقع السطر:
< %= if item.status == 1 do % >
...
< % else % >
...
< % end % >استبدله بما يلي:
< %= if item.status == 1 do % >
< .link href={~p"/items/toggle/#{item.id}"}
class="toggle checked" >
type="checkbox"
</ .link >
< % else % >
< .link href={~p"/items/toggle/#{item.id}"}
type="checkbox"
class="toggle" >
</ .link >
< % end % > عند النقر فوق هذا الرابط ، يتم استدعاء نقطة نهاية get /items/toggle/:id ،
وهذا بدوره يؤدي إلى toggle/2 معالج حددنا أعلاه.
قبل:
/lib/app_web/controllers/item_html/index.html.heex#L40
بعد:/lib/app_web/controllers/item_html/index.html.heex#L47-L57
app.scss .checked لسوء الحظ ، لا يمكن أن تحتوي العلامات <a> (التي يتم إنشاؤها باستخدام <.link> ) على :checked ، وبالتالي فإن أنماط TODODVC الافتراضية التي عملت على علامة <input> لن تعمل من أجل الرابط. لذلك نحن بحاجة إلى إضافة خطين من CSS إلى app.scss لدينا.
افتح ملف assets/css/app.scss وأضف الأسطر التالية إليه:
. todo-list li . checked + label {
background-image : url ( 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E' );
background-repeat : no-repeat;
} بعد حفظ الملف ، يجب أن يكون لديك: /assets/css/app.scss#L8 app.scss#l8
وعندما تشاهد التطبيق ، تعمل وظيفة التبديل كما هو متوقع:

ملاحظة التنفيذ : نحن لا نستخدم JavaScript عن عمد شديد لأننا نوضح كيفية القيام بتطبيق من جانب الخادم بنسبة 100 ٪. يعمل هذا دائمًا حتى عندما يتم تعطيل JS في المتصفح أو أن الجهاز قديم جدًا ولا يحتوي على متصفح ويب حديث. كان بإمكاننا بسهولة إضافة سمة onclick إلى علامة <input> ، على سبيل المثال:
< input < %= checked(item) % > type="checkbox" class="toggle"
onclick="location.href='
< %= Routes.item_path(@conn, :toggle, item.id) % > ';" > لكن onclick هو JavaScript ولا نحتاج إلى اللجوء إلى JS .
<a> (الرابط) هو نهج غير JS الدلالي تمامًا لتبديل item.status .
todo إذا "أكملت" أو عادت إلى العملية ، فقد يختلف ترتيب Todos بين هذه العمليات. للحفاظ على هذا ثابت ، دعنا نجلب جميع عناصر todo بنفس الترتيب.
داخل lib/app/todo.ex ، قم بتغيير list_items/0 إلى ما يلي.
def list_items do
query =
from (
i in Item ,
select: i ,
order_by: [ asc: i . id ]
)
Repo . all ( query )
end من خلال جلب عناصر todo وطلبها ، نضمن أن UX يبقى ثابتًا!
الجزء الأخير من الوظائف التي نحتاج إلى إضافتها إلى واجهة المستخدم الخاصة بنا هي القدرة على تحرير نص العنصر.
في نهاية هذه الخطوة ، سيكون لديك تحرير في الخط العمل:

سبب طلب نقرتين لتحرير عنصر ما ، حتى لا يقوم الأشخاص بتحرير عنصر ما أثناء التمرير. لذلك يتعين عليهم النقر/النقر عمداً مرتين لتحرير.
في مواصفات TODOMVC ، يتم تحقيق ذلك عن طريق إنشاء مستمع حدث لحدث النقر المزدوج واستبدال عنصر <label> مع <input> . نحاول تجنب استخدام JavaScript في تطبيق Phoenix المقدم من جانب الخادم ( في الوقت الحالي ) ، لذلك نريد استخدام نهج بديل. لحسن الحظ ، يمكننا محاكاة حدث النقر المزدوج باستخدام فقط HTML و CSS . انظر: https://css-tricks.com/double click-in-css ( نوصي بقراءة هذا المنشور والتوضيح لفهم تمامًا كيف يعمل هذا CSS !)
ملاحظة : إن تنفيذ CSS ليس نقرة مزدوجة حقيقية ، سيكون وصفًا أكثر دقة هو "نقرة اثنين" لأن النقرتين يمكن أن تحدث مع تأخير تعسفي. IE First Click تليها Wait 10sec و Click الثانية سيكون لها نفس التأثير مثل نقرتين في تتابع سريع. إذا كنت ترغب في تنفيذ النقر المزدوج الحقيقي ، راجع: github.com/dwyl/javascript-todo-list-tutorial#52-double click
دعنا نستمر معها! افتح ملف lib/app_web/controllers/item_html/index.html.heex وتحديد موقع السطر:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % >استبدله بـ:
< % = if @ editing . id do % >
<. link href = { ~p " /items " }
method = "get"
class= "new-todo" >
Click here to create a new item!
< / . link >
< % else % >
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % >
< % end % > هنا ، نتحقق مما إذا كنا نقوم بتحرير عنصر ، ونضع رابط بدلاً من النموذج. نفعل هذا لتجنب وجود نماذج متعددة على الصفحة. إذا لم نقم بتحرير عنصر ما ، فقم بإعداد new.html.heex كما كان من قبل. مع هذا ، إذا قام المستخدم بتحرير عنصر ما ، فهو قادر على "الخروج من وضع التحرير" من خلال النقر على الرابط الذي يتم تقديمه.
على سبيل المثال: lib/app_web/controllers/item_html/index.html.heex#L30-L38
بعد ذلك ، لا يزال في ملف index.html.eex ، حدد موقع السطر:
< %= for item < - @items do % > استبدل علامة <li> بأكملها بالرمز التالي.
< li data - id = { item . id } class= { complete ( item ) } >
< % = if item . status == 1 do % >
<. link href = { ~p " /items/toggle/ #{ item . id } " }
class= "toggle checked" >
type = "checkbox"
< / . link >
< % else % >
< . link href = { ~p " /items/toggle/ #{ item . id } " }
type = "checkbox"
class= "toggle" >
< / . link >
< % end % >
< div class = "view" >
< % = if item . id == @ editing . id do % >
< % = edit (
Map . put ( assigns , :action , ~p " /items/ #{ item . id } /edit " )
|> Map . put ( :item , item )
) % >
< % else % >
< . link href = { ~p " /items/ #{ item } /edit " } class= "dblclick" >
< label > < % = item . text % > < / label >
< / . link >
< span > < / span > < ! -- used for CSS Double Click -- >
< % end % >
< . link
class = "destroy"
href = { ~p " /items/ #{ item } " }
method= "delete"
>
< / . link >
< / div >
< /li> على سبيل المثال: lib/app_web/controllers/item_html/index.html.heex#L46-L79
لقد فعلنا بعض الأشياء هنا. قمنا بتغيير زر التبديل خارج علامة <div class="view> . بالإضافة إلى ذلك ، قمنا بتغيير النص باستخدام عبارات حظر if else .
إذا لم يتم تحرير المستخدم ، فسيتم تقديم رابط ( <a> ) والذي ، عند النقر فوقه ، يسمح للمستخدم بإدخال وضع "تحرير". من ناحية أخرى ، إذا كان المستخدم يقوم بالتحرير ، فإنه يجعل ملف edit.html.heex .
عند الحديث عن ذلك ، دعنا نقوم بتحرير edit.html.heex بحيث يجعل ما نريده: حقل نص ، بمجرد الضغط Enter ، يقوم بتحرير عنصر الإحالة.
< .simple_form :let={f} for={@changeset} method="put" action={~p"/items/#{@item}"} >
< .input
field={{f, :text}}
type="text"
placeholder="what needs to be done?"
class="new-todo"
/>
< :actions >
< .button
style="display: none;"
type="submit" >
Save
</ .button >
</ :actions >
<!-- submit the form using the Return/Enter key -->
</ .simple_form >CSS للتحرير لتمكين تأثير النقر المزدوج CSS للدخول إلى وضع edit ، نحتاج إلى إضافة CSS التالية إلى assets/css/app.scss :
. dblclick {
position : relative; /* So z-index works later, but no surprises now */
}
. dblclick + span {
position : absolute;
top : -1 px ; /* these negative numbers are to ensure */
left : -1 px ; /* that the <span> covers the <a> */
width : 103 % ; /* Gotta do this instead of right: 0; */
bottom : -1 px ;
z-index : 1 ;
}
. dblclick + span : active {
left : -9999 px ;
}
. dblclick : hover {
z-index : 2 ;
} على سبيل المثال: assets/css/app.css#L13-L32
بالإضافة إلى ذلك ، نظرًا لأن ترميزنا يختلف قليلاً عن علامة TODODVC ، نحتاج إلى إضافة المزيد من CSS للحفاظ على واجهة المستخدم ثابتة:
. todo-list li . toggle + div > a > label {
background-image : url ( 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E' );
background-repeat : no-repeat;
background-position : center left;
}
. todo-list li . checked + div > a > label
{
background-image : url ( 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E' );
background-repeat : no-repeat;
background-position : center left;
}
. toggle {
width : 10 % ;
z-index : 3 ; /* keep the toggle checkmark above the rest */
}
a . new-todo {
display : block;
text-decoration : none;
}
. todo-list . new-todo {
border : 1 px # 1abc9c solid;
}
. view a , . view a : visited {
display : block;
text-decoration : none;
color : # 2b2d2f ;
}
. todo-list li . destroy {
text-decoration : none;
text-align : center;
z-index : 3 ; /* keep the delete link above the text */
} هذا هو ما يجب أن يبدو عليه ملف app.scss في نهاية هذه الخطوة: assets/css/app.css#L34-L71
ItemController.edit/2 من أجل تمكين التحرير داخل الخط ، نحتاج إلى تعديل وظيفة edit/2 . افتح ملف lib/app_web/controllers/item_controller.ex واستبدل وظيفة edit/2 بما يلي:
def edit ( conn , params ) do
index ( conn , params )
end بالإضافة إلى ذلك ، بالنظر إلى أننا نطلب من وظيفة index/2 التعامل مع التحرير ، نحتاج إلى تحديث index/2 :
def index ( conn , params ) do
item = if not is_nil ( params ) and Map . has_key? ( params , "id" ) do
Todo . get_item! ( params [ "id" ] )
else
% Item { }
end
items = Todo . list_items ( )
changeset = Todo . change_item ( item )
render ( conn , "index.html" , items: items , changeset: changeset , editing: item )
end أخيرًا ، نحتاج إلى التعامل مع تقديم النموذج لتحديث عنصر ما (يتم تقديمه في edit.html.heex ). عندما نضغط على Enter ، يتم استدعاء معالج update/2 داخل lib/app_web/controllers/item_controller.ex . نريد البقاء على نفس الصفحة بعد تحديث العنصر.
لذا ، قم بتغييره بحيث يبدو هكذا.
def update ( conn , % { "id" => id , "item" => item_params } ) do
item = Todo . get_item! ( id )
case Todo . update_item ( item , item_params ) do
{ :ok , _item } ->
conn
|> redirect ( to: ~p " /items/ " )
{ :error , % Ecto.Changeset { } = changeset } ->
render ( conn , :edit , item: item , changeset: changeset )
end
end يجب الآن أن يبدو ملف item_controller.ex الخاص بك: lib/app_web/controllers/item_controller.ex
ItemControllerTestفي سعينا لإنشاء تطبيق صفحة واحدة ، كسرنا بعض الاختبارات! وهذا موافق. من السهل إصلاحها.
افتح test/app_web/controllers/item_controller_test.exs وتحديد موقع الاختبار بالنص التالي.
test "renders form for editing chosen item"
وتغييره بحيث يبدو ما يلي.
test "renders form for editing chosen item" , % { conn: conn , item: item } do
conn = get ( conn , ~p " /items/ #{ item } /edit " )
assert html_response ( conn , 200 ) =~ "Click here to create a new item"
end عندما ندخل "تحرير وضع مؤقت" ، نقوم بإنشاء <a> رابط للعودة إلى /items ، كما قمنا بتنفيذها مسبقًا. تحتوي هذه العلامة على نص "انقر هنا لإنشاء عنصر جديد" ، وهو ما نؤكده.
على سبيل المثال: test/app_web/controllers/item_controller_test.exs#L37-L39
بعد ذلك ، حدد الاختبار مع الوصف التالي:
describe "update item"قم بتحديث الكتلة إلى قطعة التعليمات البرمجية التالية.
describe "update item" do
setup [ :create_item ]
test "redirects when data is valid" , % { conn: conn , item: item } do
conn = put ( conn , ~p " /items/ #{ item } " , item: @ update_attrs )
assert redirected_to ( conn ) == ~p " /items/ "
conn = get ( conn , ~p " /items/ " )
assert html_response ( conn , 200 ) =~ "some updated text"
end
test "errors when invalid attributes are passed" , % { conn: conn , item: item } do
conn = put ( conn , ~p " /items/ #{ item } " , item: @ invalid_attrs )
assert html_response ( conn , 200 ) =~ "can't be blank"
end
end على سبيل المثال: test/app_web/controllers/item_controller_test.exs#L67-L80
لقد قمنا بتحديث المسارات التي يعيدها التطبيق بعد تحديث عنصر ما. نظرًا لأننا نقوم ببناء تطبيق من صفحة واحدة ، فإن هذا المسار يتعلق بمسار /items/ URL.
إذا قمت بإجراء الاختبارات الآن ، فيجب أن تمر مرة أخرى:
mix test
23:08:01.785 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failures
Randomized with seed 956565
index.html الآن بعد أن أصبح لدينا ميزات toggle edit العمل ، يمكننا أخيرًا إزالة تخطيط Phoenix (الجدول) الافتراضي من قالب lib/app_web/controllers/item_html/index.html.heex .

افتح ملف lib/app_web/controllers/item_html/index.html.eex وإزالة جميع التعليمات البرمجية قبل السطر:
< section class =" todoapp " > على سبيل المثال: lib/app_web/controllers/item_html/index.html.heex
يجب أن يبدو تطبيقك الآن هكذا: 
لسوء الحظ ، من خلال إزالة التصميم الافتراضي ، قمنا "بقطع" الاختبارات.
افتح test/app_web/controllers/item_controller_test.exs وتحديد موقع الاختبار الذي يحتوي على الوصف التالي:
test "lists all items"تحديث التأكيد من:
assert html_response ( conn , 200 ) =~ "Listing Items"ل:
assert html_response ( conn , 200 ) =~ "todos" على سبيل المثال: test/app_web/controllers/item_controller_test.exs#L14
الآن بعد أن تعمل وظيفة Core (Create ، Edit/Update ، Delete) ، يمكننا إضافة تحسينات واجهة المستخدم النهائية. في هذه الخطوة ، سنضيف التنقل/التصفية.

عرض "الكل" هو الافتراضي. "النشط" هو جميع العناصر مع status==0 . "الانتهاء" هي جميع العناصر مع status==1 .
/:filter مسارقبل البدء ، دعنا نضيف اختبار الوحدة. نريد أن نعرض العناصر المصفاة وفقًا للمرشح المختار.
افتح test/app_web/controllers/item_controller_test.exs وتحديد موقع describe "index" do . في هذه الكتلة ، أضف الاختبار التالي. يتحقق مما إذا كان العنصر يتم عرضه بشكل صحيح عند تغيير المرشح.
test "lists items in filter" , % { conn: conn } do
conn = post ( conn , ~p " /items " , item: @ public_create_attrs )
# After creating item, navigate to 'active' filter page
conn = get ( conn , ~p " /items/filter/active " )
assert html_response ( conn , 200 ) =~ @ public_create_attrs . text
# Navigate to 'completed page'
conn = get ( conn , ~p " /items/filter/completed " )
assert ! ( html_response ( conn , 200 ) =~ @ public_create_attrs . text )
end على سبيل المثال: test/app_web/controllers/item_controller_test.exs#L21-L32
افتح lib/app_web/router.ex وأضف المسار التالي:
get "/items/filter/:filter" , ItemController , :index على سبيل المثال: /lib/app_web/router.ex#L23
index/2 لإرسال filter للعرض/القالب افتح ملف lib/app_web/controllers/item_controller.ex وتحديد وظيفة index/2 . استبدل استدعاء render/3 في نهاية index/2 بما يلي:
render ( conn , "index.html" ,
items: items ,
changeset: changeset ,
editing: item ,
filter: Map . get ( params , "filter" , "all" )
) على سبيل المثال: lib/app_web/controllers/item_controller.ex#L17-L22
Map.get(params, "filter", "all") يعين القيمة الافتراضية filter الخاص بنا إلى "All" لذلك عند تقديم index.html ، إظهار "جميع العناصر".
filter/2 طريقة عرض من أجل تصفية العناصر حسب حالتها ، نحتاج إلى إنشاء وظيفة جديدة.
افتح ملف lib/app_web/controllers/item_html.ex وإنشاء وظيفة filter/2 على النحو التالي:
def filter ( items , str ) do
case str do
"items" -> items
"active" -> Enum . filter ( items , fn i -> i . status == 0 end )
"completed" -> Enum . filter ( items , fn i -> i . status == 1 end )
_ -> items
end
end على سبيل المثال: lib/app_web/controllers/item_html.ex#L19-L26
سيسمح لنا هذا بتصفية العناصر في الخطوة التالية.
index.html استخدم وظيفة filter/2 لتصفية العناصر التي يتم عرضها. افتح ملف lib/app_web/controllers/item_html/index.html.heex وتحديد for خط الحلقة:
< % = for item <- @ items do % >استبدله بـ:
< % = for item <- filter ( @ items , @ filter ) do % > على سبيل المثال: lib/app_web/controllers/item_html/index.html.heex#L18
هذا يستدعي وظيفة filter/2 التي حددناها في الخطوة السابقة التي تمريرها في قائمة @items و @filter المحددة.
بعد ذلك ، حدد موقع <footer> واستبدل محتويات <ul class="filters"> مع الكود التالي:
< li >
< % = if @ filter == "items" do % >
<a href = "/items/filter/items" class= "selected" >
All
< / a >
< % else % >
<a href = "/items/filter/items" >
All
< / a >
< % end % >
< / li >
< li >
< % = if @ filter == "active" do % >
<a href = "/items/filter/active" class= 'selected' >
Active
[ < % = Enum . count ( filter ( @ items , "active" ) ) % > ]
< / a >
< % else % >
<a href = "/items/filter/active" >
Active
[ < % = Enum . count ( filter ( @ items , "active" ) ) % > ]
< / a >
< % end % >
< / li >
< li >
< % = if @ filter == "completed" do % >
<a href = "/items/filter/completed" class= 'selected' >
Completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a >
< % else % >
<a href = "/items/filter/completed" >
Completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a >
< % end % >
< / li > نحن نضيف بشكل مشروط الفئة selected وفقًا لقيمة تعيين @filter .
على سبيل المثال: /lib/app_web/controllers/item_html/index.html.heex#L62-L98
في نهاية هذه الخطوة ، سيكون لديك مرشح تذييل يعمل بالكامل:

يمكننا تغطية هذه الوظيفة بسرعة مع اختبار وحدة صغيرة. افتح test/app_web/controllers/item_html_test.exs وأضف ما يلي.
test "test filter function" do
items = [
% { text: "one" , status: 0 } ,
% { text: "two" , status: 0 } ,
% { text: "three" , status: 1 } ,
% { text: "four" , status: 2 } ,
% { text: "five" , status: 2 } ,
% { text: "six" , status: 1 } ,
]
assert length ( ItemHTML . filter ( items , "items" ) ) == 4
assert length ( ItemHTML . filter ( items , "active" ) ) == 2
assert length ( ItemHTML . filter ( items , "completed" ) ) == 2
assert length ( ItemHTML . filter ( items , "any" ) ) == 4
endويجب أن يتم ذلك مع هذه الميزة؟ عمل رائع!
لقد انتهينا تقريبًا من خلال تنفيذ Phoenix الخاص بنا لـ TODOMVC. آخر شيء يتم تنفيذه هو "واضح مكتمل".
افتح ملف lib/app_web/router.ex الخاص بك وأضف المسار التالي:
get "/items/clear" , ItemController , :clear_completed يجب الآن أن يبدو scope "/" مثل ما يلي:
scope "/" , AppWeb do
pipe_through :browser
get "/" , PageController , :home
get "/items/toggle/:id" , ItemController , :toggle
get "/items/clear" , ItemController , :clear_completed
get "/items/filter/:filter" , ItemController , :index
resources "/items" , ItemController
end في ملف lib/app_web/controllers/item_controller.ex أضف الرمز التالي:
import Ecto.Query
alias App.Repo
def clear_completed ( conn , _param ) do
person_id = 0
query = from ( i in Item , where: i . person_id == ^ person_id , where: i . status == 1 )
Repo . update_all ( query , set: [ status: 2 ] )
# render the main template:
index ( conn , % { filter: "all" } )
end على سبيل المثال: lib/app_web/controllers/item_controller.ex#L87-L93
يستخدم هذا دالة update_all/3 المتانعة لتحديث جميع العناصر التي تتطابق مع query . في حالتنا ، نبحث عن جميع items التي تنتمي إلى person_id==0 ولدينا status==1 .
نحن لا نحذف العناصر ، بل نقوم بتحديث حالتها إلى 2 والتي تعني لأغراض مثالنا أنها "مؤرشفة".
ملاحظة : هذا دليل مفيد لـ
update_all: https://adamdelong.com/bulk-update-ecto
أخيرًا ، في lib/app_web/controllers/item_html/index.html.eex قم بالتمرير إلى أسفل الملف واستبدل السطر:
< button class = "clear-completed" style = "display: block;" >
Clear completed
< / button >مع:
< a class = "clear-completed" href = "/items/clear" >
Clear completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a > على سبيل المثال: lib/app_web/controllers/item_html/index.html.heex#L104-L107
آخر شيء نحتاج إلى القيام به هو تحديث وظيفة filter/2 داخل lib/app_web/controllers/item_html.ex . Since status = 2 now pertains to an archived state, we want to return anything that is not archived.
Change the filter/2 function so it looks like so.
def filter ( items , str ) do
case str do
"items" -> Enum . filter ( items , fn i -> i . status !== 2 end )
"active" -> Enum . filter ( items , fn i -> i . status == 0 end )
"completed" -> Enum . filter ( items , fn i -> i . status == 1 end )
_ -> Enum . filter ( items , fn i -> i . status !== 2 end )
end
endAt the end of this section your Todo List should have the "Clear completed" function working:

It's useful to have tests cover this feature. Open test/app_web/controllers/item_controller_test.exs . Alongside the constants, on top of the file, add the following line.
@completed_attrs %{person_id: 42, status: 1, text: "some text completed"}
We will use this to create an item that is already completed, so we can test the "clear completed" functionality.
Add the next lines to test the clear_completed/2 function.
describe "clear completed" do
setup [ :create_item ]
test "clears the completed items" , % { conn: conn , item: item } do
# Creating completed item
conn = post ( conn , ~p " /items " , item: @ public_completed_attrs )
# Clearing completed items
conn = get ( conn , ~p " /items/clear " )
items = conn . assigns . items
[ completed_item | _tail ] = conn . assigns . items
assert conn . assigns . filter == "all"
assert completed_item . status == 2
end
test "clears the completed items in public (person_id=0)" , % { conn: conn , item: item } do
# Creating completed item
conn = post ( conn , ~p " /items " , item: @ public_completed_attrs )
# Clearing completed items
conn = get ( conn , ~p " /items/clear " )
items = conn . assigns . items
[ completed_item | _tail ] = conn . assigns . items
assert conn . assigns . filter == "all"
assert completed_item . status == 2
end
endAt this point we already have a fully functioning Phoenix Todo List. There are a few things we can tidy up to make the App even better!
If you are the type of person to notice the tiny details, you would have been itching each time you saw the " 1 items left " in the bottom left corner:

Open your test/app_web/controllers/item_html_test.exs file and add the following test:
test "pluralise/1 returns item for 1 item and items for < 1 <" do
assert ItemHTML . pluralise ( [ % { text: "one" , status: 0 } ] ) == "item"
assert ItemHTML . pluralise ( [
% { text: "one" , status: 0 } ,
% { text: "two" , status: 0 }
] ) == "items"
assert ItemHTML . pluralise ( [ % { text: "one" , status: 1 } ] ) == "items"
end eg: test/app_web/controllers/item_html_test.exs#L28-L35
This test will obviously fail because the AppWeb.ItemHTML.pluralise/1 is undefined. Let's make it pass!
Open your lib/app_web/controllers/item_html.ex file and add the following function definition for pluralise/1 :
# pluralise the word item when the number of items is greater/less than 1
def pluralise ( items ) do
# items where status < 1 is equal to Zero or Greater than One:
case remaining_items ( items ) == 0 || remaining_items ( items ) > 1 do
true -> "items"
false -> "item"
end
end eg: lib/app_web/controllers/item_html.ex#L28-L35
Note : we are only pluralising one word in our basic Todo App so we are only handling this one case in our
pluralise/1function. In a more advanced app we would use a translation tool to do this kind of pluralising. See: https://hexdocs.pm/gettext/Gettext.Plural.html
Finally, use the pluralise/1 in our template. Open lib/app_web/controllers/item_html/index.html.heex
Locate the line:
< span class = "todo-count" > < % = remaining_items ( @ items ) % > items left < /span>And replace it with the following code:
< span class = "todo-count" >
< % = remaining_items ( @ items ) % > < % = pluralise ( @ items ) % > left
< /span> eg: lib/app_web/controllers/item_html/index.html.heex#L61
At the end of this step you will have a working pluralisation for the word item/items in the bottom left of the UI:

If you visit one of the TodoMVC examples, you will see that no <footer> is displayed when there are no items in the list: http://todomvc.com/examples/vanillajs

At present our App shows the <footer> even if their are Zero items: ?

This is a visual distraction/clutter that creates unnecessary questions in the user's mind. Let's fix it!
Open your lib/app_web/controllers/item_html.ex file and add the following function definition unarchived_items/1 :
def got_items? ( items ) do
Enum . filter ( items , fn i -> i . status < 2 end ) |> Enum . count > 0
end eg: lib/app_web/controllers/item_html.ex#L37-L39
Now use got_items?/1 in the template.
Wrap the <footer> element in the following if statement:
< % = if got_items? ( @ items ) do % >
< % end % > eg: lib/app_web/controllers/item_html/index.html.heex#L58
The convention in Phoenix/Elixir ( which came from Ruby/Rails ) is to have a ? (question mark) in the name of functions that return a Boolean ( true/false ) result.
At the end of this step our <footer> element is hidden when there are no items:

/ to ItemController.index/2 The final piece of tidying up we can do is to change the Controller that gets invoked for the "homepage" ( / ) of our app. Currently when the person viewing the Todo App
visits http://localhost:4000/ they see the lib/app_web/controllers/page_html/home.html.eex template:

This is the default Phoenix home page ( minus the CSS Styles and images that we removed in step 3.4 above ). It does not tell us anything about the actual app we have built, it doesn't even have a link to the Todo App! Let's fix it!
Open the lib/app_web/router.ex file and locate the line:
get "/" , PageController , :index Update the controller to ItemController .
get "/" , ItemController , :index eg: lib/app_web/router.ex#L20
Now when you run your App you will see the todo list on the home page:

Unfortunately, this update will "break" the page test. Run the tests and see:
1) test GET / (AppWeb.PageControllerTest)
test/app_web/controllers/page_controller_test.exs:4
Assertion with = ~ failed
code: assert html_response(conn, 200) = ~ " Welcome to Phoenix! "
left: " <!DOCTYPE html>n<html lang= " en " >n <head>n ... " Given that we are no longer using the Page Controller, View, Template or Tests, we might as well delete them from our project!
git rm lib/app_web/controllers/page_controller.ex
git rm lib/app_web/controllers/page_html.ex
git rm lib/app_web/page_html/home.html.heex
git rm test/app_web/controllers/page_controller_test.exs Deleting files is good hygiene in any software project. Don't be afraid to do it, you can always recover files that are in your git history.
Re-run the tests:
mix test
You should see them pass now:
...........................
Finished in 0.5 seconds
27 tests, 0 failures
Given that our Phoenix Todo List App is 100% server rendered, older browsers will perform a full page refresh when an action (create/edit/toggle/delete) is performed. This will feel like a "blink" in the page and on really slow connections it will result in a temporary blank page ! Obviously, that's horrible UX and is a big part of why Single Page Apps (SPAs) became popular; to avoid page refresh, use Turbo !
Get the performance benefits of an SPA without the added complexity of a client-side JavaScript framework. When a link is clicked/tapped, Turbolinks automatically fetches the page, swaps in its <body> , and merges its <head> , all without incurring the cost of a full page load.
Luckily, adding Turbo will require just a simple copy and paste! Check the unpkg files to fetch the latest CDN package.
We now need to add the following line to lib/app_web/components/layouts/app.html.heex and lib/app_web/components/layouts/root.html.heex .
< script src =" https://unpkg.com/browse/@hotwired/[email protected]/dist/turbo.es2017-esm.js " > </ script > This will install the UMD builds from Turbo without us needing to install a package using npm . Neat, huh?
And that's it! Now when you deploy your server rendered Phoenix App, it will feel like an SPA! Try the Fly.io demo again: phxtodo.fly.dev Feel that buttery-smooth page transition.
Currently, our application occurs in the same page. However, there is a route that we don't use and is also aesthetically incompatible with the rest of our app.

If we check lib/app_web/controllers/item_controller.ex , you might notice the following function.
def show ( conn , % { "id" => id } ) do
item = Todo . get_item! ( id )
render ( conn , :show , item: item )
end This serves the GET /items/:id route. We could do the same as we did with edit and render index . However, let's do something different so we learn a bit more about routes.
If we head on to router.ex , and locate the line:
resources "/items" , ItemControllerWe can change it to this.
resources "/items" , ItemController , except: [ :show ] We are saying that we want to keep all the routes in ItemController except the one related to the show action.
We can now safely delete it from item_controller.ex , as we don't need it any more.
Your files should look like the following.
eg: /lib/router.ex#L19-L29 lib/app_web/controllers/item_controller.ex
Currently, the application allows anyone to access it and manage todo items . Wouldn't it be great if we added authentication so each person could check their own list?
We created a dedicated authentication guide: /auth.md to help you set this up. You will soon find out this is extremely easy ?.
Deployment to Fly.io takes a few minutes, but has a few "steps", we suggest you follow the speed run guide: https://fly.io/docs/elixir/getting-started/
Once you have deployed you will will be able to view/use your app in any Web/Mobile Browser.
eg: https://phxtodo.fly.dev
xs

Our Phoenix server currently only returns HTML pages that are server-side rendered . This is already awesome but we can make use of Phoenix to extend its capabilities.
What if our server also responded with JSON ? You're in luck! We've created small guide for creating a REST API : api.md
If you found this example useful, please ️ the GitHub repository so we ( and others ) know you liked it!
If you want to learn more Phoenix and the magic of LiveView , consider reading our beginner's tutorial: github.com/dwyl/ phoenix-liveview-counter-tutorial
Thank you for learning with us! ☀️