Продолжаем публиковать перевод туториала из официального руководства Ember.js. Туториал состоит из двух частей и это вторая половина первой части туториала. Напоминаем, что первую половину вы можете прочитать по этой ссылке Список тем, которые покрывает туториал внушает: Садитесь поудобнее, открывайте терминалы, находите проект на своем компьютере и давайте двигаться дальше. И помните, что если у вас возникнут трудности, всегда можно попросить помощи в Discord канале сообщества (на русском канал #lang-russian), а также в русскоязычном телеграмм канале ember_js Пора наконец-то поработать над списком аренды:  Составляя этот список арендуемой недвижимости, вы узнаете о: Начнем с создания компонента <Rental>. На этот раз мы будем использовать генератор компонентов для создания шаблона и тестового файла для нас: Генератор создал для нас два новых файла: шаблон компонента в app/components/rental.hbs и тестовый файл компонента в tests/integration/components/rental-test.js. Мы начнем с редактирования шаблона. Давайте пока захардкодим детали для одного арендуемого объекта, а потом заменим его реальными данными с сервера.  Затем мы напишем тест, чтобы убедиться, что все детали присутствуют. Мы заменим шаблонный тест, сгенерированный для нас, нашими собственными командами, как мы делали это ранее для компонента <Jumbo> :  Тесты должны пройти.  Наконец, давайте добавим новый компонент в шаблон индекса, чтобы заполнить страницу.  При этом мы должны увидеть компонент <Rental> показывающий наш Grand Old Mansion три раза на странице:  Все выглядит довольно неплохо для небольшой работы! Далее добавим изображение для арендуемой недвижимости. Мы будем использовать генератор компонентов для этого снова: На этот раз у нас был / в названии компонента. Это привело к созданию компонента в app/components/rental/image.hbs, который может быть вызван как <Rental::Image> . Подобные компоненты известны как компоненты из пространства имен. Пространство имен позволяет нам организовывать наши компоненты по папкам в соответствии с их назначением. Это совершенно необязательно, но удобно, особенно, когда вы разрабатываете крупное приложение в коллективе. Давайте отредактируем шаблон компонента:  Вместо того, чтобы жестко задавать конкретные значения для атрибутов src и alt в <img>, мы выбрали ключевое слово ...attributes, которое также иногда называют «splattributes». Это позволяет передавать произвольные атрибуты HTML при вызове этого компонента, например так:  Мы указали здесь атрибут HTML src и alt, который будет передан компоненту и присоединен к элементу, где ...attributes применяются в шаблоне компонента. Вы можете думать, что это похоже на {{yield}}, но только для атрибутов HTML, а не для отображаемого содержимого. Фактически, мы уже использовали эту функцию ранее, когда передавали атрибут class в <LinkTo>.  Таким образом, наш компонент <Rental::Image> не связан с какой-либо конкретной арендуемой собственностью на сайте. Конечно, у нас все также жестко закодировано, но мы с этим скоро разберемся. Пока же ограничим весь хардкод компонентом , чтобы было легче очистить его, когда мы перейдем к извлечению реальных данных.
 В общем, это хорошая идея добавить ...attributes к корневому элементу в вашем компоненте. Это обеспечит максимальную гибкость, так как инициатору может потребоваться передать классы для стилизации или атрибуты ARIA для улучшения доступности. А теперь давайте напишем тест для нашего нового компонента!  Наконец, мы также должны обновить тесты для компонента <Rental> чтобы убедиться, что мы успешно вызвали <Rental::Image>.  Поскольку мы уже написали тесты, относящиеся к <Rental::Image>, здесь мы можем опустить детали и свести проверку к минимуму. Таким образом, нам также не придется обновлять тест <Rental> всякий раз, когда мы вносим изменения в <Rental::Image> .  В этой главе вы добавите интерактивность на страницу, которая позволит пользователю кликать на изображение, чтобы увеличить или уменьшить его:  При этом вы узнаете о: Пока что все компоненты, которые мы написали, являются чисто презентационными — это просто фрагменты разметки многократного использования. Это конечно, прекрасно, но в Ember компоненты могут сделать намного больше! Иногда вы хотите связать некоторое поведение с вашими компонентами, чтобы они могли делать более интересные вещи. Например, <LinkTo> может реагировать на клики, изменяя URL-адрес и переходя на другую страницу. Здесь мы собираемся сделать что-то похожее! Мы собираемся реализовать функциональность «View Larger» и «View Smaller», которая позволит нашим пользователям щелкнуть на изображение дома, просмотреть увеличенную версию, и снова щелкнуть по нему, чтобы вернуться к уменьшенной версии. Другими словами, нам нужен способ переключения изображения между одним из двух состояний. Чтобы сделать это, нам нужен способ для компонента хранить два возможных состояния и знать, в каком состоянии он находится в данный момент. Ember дополнительно позволяет нам связать код JavaScript с компонентом именно для этой цели. Мы можем добавить файл JavaScript для нашего компонента <Rental::Image>, запустив генератор компонента: Эта команда сгенерировала файл JavaScript с тем же именем, что и шаблон нашего компонента в app/components/rentals/image.js. Он содержит класс JavaScript, унаследованный от @glimmer/component. Ember создает экземпляр класса всякий раз, когда вызывается наш компонент. Мы можем использовать этот экземпляр для хранения нашего состояния:  Здесь, в конструкторе компонента, мы инициализировали переменную экземпляра this.isLarge со значением false, так как это состояние по умолчанию, которое мы хотим для нашего компонента. Давайте обновим наш шаблон, чтобы использовать это состояние, которое мы только что добавили:  В шаблоне у нас есть доступ к переменным экземпляра компонента. Условный синтаксис {{#if ...}}...{{else}}...{{/if}} позволяет отображать различное содержимое в зависимости от условия (в данном случае это значение переменной экземпляра this.isLarge). Комбинируя эти две функции, мы можем соответственно визуализировать как маленькую, так и большую версию изображения. Мы можем это проверить временно изменив начальное значение в нашем файле JavaScript. Если мы изменим app/components/rental/image.js для инициализации this.isLarge = true; в конструкторе мы должны увидеть большую версию изображения свойства в браузере. Здорово!  После того как мы проверили, можно изменить this.isLarge обратно на false. Поскольку этот шаблон инициализации переменных экземпляра в конструкторе довольно распространен, для него существует гораздо более краткий синтаксис:  Такая же функциональность, но намного короче! Конечно, наши пользователи не могут редактировать наш исходный код, поэтому нам нужен способ для переключения размера изображения из браузера. В частности, мы хотим переключать значение this.isLarge всякий раз, когда пользователь нажимает на наш компонент. Давайте изменим наш класс, добавив метод для переключения размера:  Мы сделали несколько вещей здесь, поэтому давайте разберемся с этим. Сначала мы добавили декоратор @tracked к переменной экземпляра isLarge. Эта аннотация указывает Ember отслеживать эту переменную на наличие обновлений. Всякий раз, когда значение этой переменной изменяется, Ember автоматически перерисовывает любые шаблоны, которые зависят от ее значения. В нашем случае всякий раз, когда мы присваиваем новое значение this.isLarge, аннотация @tracked заставит Ember переоценить условие {{#if this.isLarge}} в нашем шаблоне и переключиться между двумя блоками соответственно. Затем мы добавили метод toggleSize в наш класс, который переключает this.isLarge в противоположность его текущему состоянию ( false становится true или true становится false). Наконец, мы добавили декоратор @action в наш метод. Это указывает Ember, что мы намерены использовать этот метод из нашего шаблона. Без этого метод не будет функционировать должным образом в качестве функции обработки событий (в данном случае обработчика кликов). Теперь пришло время использовать это в шаблоне:  Мы изменили две вещи. Во-первых, поскольку мы хотели сделать наш компонент интерактивным, мы переключили содержащий тег с <div> на <button> (это важно из соображений accessibility). Используя правильный семантический тег, мы также «бесплатно» получим возможность фокуса и взаимодействие с клавиатурой. Затем мы использовали модификатор {{on}}, чтобы прикрепить this.toggleSize в качестве обработчика нажатия кнопки. Таким образом, мы создали наш первый интерактивный компонент. Попробуйте как он работает в браузере!  Наконец, давайте напишем тест для этого нового поведения: 
 Результат отработки теста
 Давайте почистим наш шаблон, прежде чем двигаться дальше. Мы добавили много дубликатов, когда вставляли условные выражения в шаблон. Если мы посмотрим внимательно, единственные вещи, которые отличаются между этими двумя блоками:
1) Наличие "large" CSS-класса в <button>.
2) Текст «Увеличить» и «Уменьшить». Эти изменения скрыты в большом количестве дублированного кода. Мы можем уменьшить дублирование, используя вместо этого выражение {{if}}:  Версия выражения {{if}} принимает два аргумента. Первым аргументом является условие. Второй аргумент — это выражение (expression), которое должно выполнится, если условие истинно. Необязательно, {{if}} может принимать в качестве третьего аргумента выражение, которое должно выполнится, если условие ложно. Это означает, что мы могли бы переписать метку кнопки следующим образом:  Является ли это улучшением ясности нашего кода — вопрос вкуса. В любом случае, мы значительно сократили дублирование в нашем коде и сделали важные фрагменты логики более заметными. Запустите тесты в последний раз, чтобы убедиться, что наш рефакторинг ничего не сломал, и мы будем готовы к следующему вызову!  Оставшаяся нереализованная часть в компоненте  — это карта, показывающая местоположение жилища, над чем мы будем работать дальше:
  При добавлении карты вы узнаете об: Мы будем использовать Mapbox API для создания карт для наших арендуемых объектов. Вы можете зарегистрироваться бесплатно и без кредитной карты. Mapbox предоставляет API статических изображений карт, который обслуживает изображения карт в формате PNG. Это означает, что мы можем сгенерировать соответствующий URL для параметров, которые мы хотим, и отобразить карту, используя стандартный <img>. Класс! Если вам интересно, вы можете изучить варианты, доступные в Mapbox, с помощью интерактивной песочницы. После того, как вы зарегистрировались в службе, возьмите свой общедоступный токен (default public token) и вставьте его в config/environment.js: 
 Как следует из названия, config/environment.js используется для настройки нашего приложения и хранения ключей API, подобных этим. К этим значениям можно получить доступ из других частей нашего приложения, и они могут иметь различные значения в зависимости от текущей среды (environment) (которая может быть разработкой (development), тестированием (test) или продакшнм (production)). После сохранения изменений в нашем файле конфигурации нам нужно будет перезапустить наш сервер разработки, чтобы получить эти изменения файла. В отличие от файлов, которые мы редактировали, config/environment.js не перезагружается автоматически. Вы можете остановить сервер, найдя окно терминала, где работает ember server, затем нажмите Ctrl + C То есть, нажимая клавишу «C» на клавиатуре, одновременно удерживая клавишу «Ctrl». Как только он остановился, вы можете запустить его снова с помощью той же команды ember server. Добавив в приложение ключ Mapbox API, давайте сгенерируем новый компонент для нашей карты. Поскольку не каждый компонент обязательно будет иметь определенное поведение, связанное с ним, генератор компонентов по умолчанию не создает для нас JavaScript файл. Как мы видели ранее, мы всегда можем использовать генератор компонента, чтобы добавить его позже. Однако, в случае нашего компонента <Map>, мы почти уверены, что нам понадобится файл JavaScript для некоего поведения, которое нам еще предстоит определить! Поэтому мы можем передать флаг --with-component-class генератору компонентов, чтобы у нас было все необходимое с самого начала. Давайте начнем с нашего файла JavaScript:  Здесь мы импортируем токен доступа из файла конфигурации и возвращаем его из геттера (getter) token. Это позволяет нам получить доступ к нашему токену как this.token как внутри MapComponent класса, так и в шаблоне компонента. Также важно кодировать (encode) токен, на случай, если он содержит какие-либо специальные символы, которые не безопасны для URL. Теперь перейдем от JavaScript файла к шаблону:  Во-первых, у нас есть элемент контейнера для стилизации. Затем у нас есть <img> для запроса и рендеринга статического изображения карты из Mapbox. Наш шаблон содержит несколько значений, которых еще не существует — @lat, @lng, @zoom, @width и @height. Это аргументы компонента <Map> которые мы предоставим ему при вызове. Параметризуя наш компонент с помощью аргументов, мы создали повторно используемый компонент, который можно вызывать из разных частей приложения и настраивать для удовлетворения потребностей в этих конкретных контекстах. Мы уже видели это в действии при использовании компонента <LinkTo> ранее; нам нужно было указать аргумент @route чтобы он знал, на какую страницу перейти. Мы предоставили разумное значение по умолчанию для атрибута alt на основе значений аргументов @lat и @lng. Вы можете заметить, что мы напрямую интерполируем значения в значение атрибута alt. Ember автоматически объединит эти интерполированные значения в строковое значение, включая выполнение любого необходимого экранирования (escaping) HTML. Затем мы использовали ...attributes, чтобы позволить вызывающему потребителю дополнительно настроить <img>, например, передать дополнительные атрибуты, такие как class, а также переопределить наш атрибут alt умолчанию более конкретным или дружественным для человека. Порядок важен! Ember применяет атрибуты в порядке их появления. Присваивая атрибут дефолтное значение alt первым ( до применения ...attributes), мы явно предоставляем вызывающему возможность предоставить более специализированный атрибут alt в соответствии с необходимым вариантом использования. Поскольку переданный атрибут alt (если таковой существует) появится после нашего, он переопределит указанное нами значение. С другой стороны, важно, чтобы мы назначили src, width и height после ...attributes, чтобы они не были случайно перезаписаны вызывающим (invoker). Атрибут src интерполирует все необходимые параметры в формат URL для API статического изображения карты Mapbox, включая URL-безопасный токен this.token. Наконец, поскольку мы используем изображение @2x "retina", мы должны указать атрибуты width и height. В противном случае, <img> будет отображаться в два раза больше того, что мы ожидали! Мы добавили довольно много кода в один компонент, поэтому давайте напишем несколько тестов! В частности, мы должны убедиться, что у нас есть тестовое покрытие для поведения переопределенных HTML-атрибутов, которое мы обсуждали выше. 

 Обратите внимание, что тест помощник hasAttribute из qunit-dom поддерживает использование регулярных выражений. Мы использовали эту функцию, чтобы подтвердить, что атрибут src начинается с https://api.mapbox.com/ (а не проверяли, чтобы он точно совпадал). Это позволяет быть достаточно уверенными в том, что код работает правильно, без излишней детализации в наших тестах. Скрестив пальцы… Давайте запустим тесты.  Супер, все тесты зеленые! Но значит ли это, что на самом деле это работает как задумано? Давайте выясним это, вызвав компонент <Map> из шаблона компонента <Rental>:  А вот и карта!  Также давайте добавим проверки в тесты <Rental> чтобы убедиться, что мы успешно отрисовали компонент <Map>.  На данный момент большая часть нашего шаблона <Map> посвящена атрибуту src тега <img>, который становится довольно длинным. Одна альтернатива — переместить это вычисление в отдельный JavaScript класс. Внутри нашего класса JavaScript у нас есть доступ к аргументам нашего компонента с помощью API this.args.*. Используя это, мы можем переместить логику вычисления URL из шаблона в новый геттер. 
 Выглядит неплохо! И все наши тесты все также проходят!  Обратите внимание, что мы не отметили наш геттер как @tracked. В отличие от переменных экземпляра, геттерам нельзя напрямую присвоить новое значение, поэтому для Ember не имеет смысла отслеживать их на предмет изменений. При этом значения, полученные геттерами, безусловно, могут измениться. В нашем случае значение, полученное нашим геттером src зависит от значений lat, lng, width, height и zoom из this.args. Всякий раз, когда эти зависимости обновляются, мы ожидаем, что {{this.src}} из нашего шаблона будет соответствующим образом обновлен. Ember делает это автоматически отслеживая любые переменные, к которым обращались при вычислении значения геттера. Пока сами зависимости помечены как @tracked, Ember точно знает, когда нужно сделать неактуальными (invalidate) и повторно перерисовать любые шаблоны, которые могут содержать любые «несвежие» и устаревшие значения получателя. Эта функция также известна как автоматическое отслеживание (auto-track). Все аргументы, к которым можно получить доступ из this.args (другими словами, this.args.* ), Неявно помечаются как @tracked суперклассом компонента Glimmer. Поскольку мы наследовали от этого суперкласса, все просто работает (Just works). Просто чтобы быть уверенным, мы можем добавить тест для этого поведения: 

 Используя специальный API для тестирования this.setProperties, мы можем передавать произвольные значения в наш компонент. Обратите внимание, что значение здесь не относится к экземпляру компонента. Мы не имеем прямого доступа или изменения внутренних состояний компонента (это было бы очень грубо!). Вместо этого this относится к специальному тестовому объекту контекста, к которому мы имеем доступ внутри помощника render. Это обеспечивает «мост» для передачи динамических значений в форме аргументов при вызове компонента. Это позволяет нам обновлять эти значения по мере необходимости из тестовой функции. После прохождения всех наших тестов мы готовы двигаться дальше!  В этой части мы удалим жестко закодированные данные из нашего компонента <Rental>. В конце концов ваше приложение будет уметь отображать реальные данные, полученные с сервера:  В этой части вы узнаете о: До этого момента данные были жестко запрограммированы в наш компонент <Rental>. Но это, как вы понимаете, временно, поскольку в конечном итоге мы хотим, чтобы наши данные поступали с сервера. Давайте продолжим и перенесем некоторые из этих жестко закодированных значений из компонента в рамках подготовки к этому шагу. Мы хотим начать работать над тем, чтобы в конечном итоге мы могли получить данные с сервера, а затем отобразить запрошенные данные в виде динамического содержимого из шаблонов. Для этого нам понадобится место, где мы сможем написать код для извлечения данных и загрузки их в маршруты. Место, где это можно сделать в Ember — файлы маршрутов. Они нам пока не нужны, потому что все наши маршруты по сути просто визуализируют статические страницы до этого момента, но мы сейчас мы это изменим. Начнем с создания файла маршрута для индекса маршрута. Мы создадим новый файл в app/routes/index.js со следующим содержимым:  Здесь происходит много всего, чего мы раньше не видели, так что давайте сделаем пояснения. Сначала мы импортируем класс Route в файл. Этот класс используется в качестве отправной точки для добавления функциональности в маршрут, такой как загрузка данных. Затем мы наследуем от класса Route наш собственный IndexRoute, который мы также экспортируем, чтобы остальная часть приложения могла его использовать. Выглядит неплохо. Но что происходит внутри этого класса маршрута? Мы реализовали асинхронный метод под названием model(). Этот метод также известен как хук модели. Хук модели отвечает за запрос и подготовку любых данных, которые вам нужны для данного маршрута. Ember автоматически вызовет эту функцию при переходе на этот маршрут, чтобы у вас была возможность запустить собственный код для получения нужных вам данных. Объект, возвращаемый из этого хука, называется моделью для маршрута (что логично). Обычно это то место, где мы запрашиваем данные с сервера. Поскольку запрос данных обычно является асинхронной операцией, хук модели помечается как async. Это дает нам возможность использовать ключевое слово await для ожидания завершения операций извлечения данных. (от переводчика: также надо помнить, что результат асинхронной функции будет обернут в Promise под капотом Javascript) Мы вернемся к этому позже. В данный момент из модели мы просто возвращаем те же данные, что мы жестко закодировали в компоненте , но в формате объекта JavaScript (также известного как POJO (Plain Old Javascript Object)).
 Итак, теперь, когда мы подготовили некоторые данные модели для нашего маршрута, давайте использовать их в нашем шаблоне. В шаблонах маршрутов мы можем получить доступ к модели маршрута через переменную @model. В нашем случае это будет содержать POJO, возвращенный из нашего хука модели. Чтобы проверить, что это работает, давайте изменим наш шаблон и попробуем отобразить свойство title из нашей модели:  Если мы посмотрим на нашу страницу в браузере, мы должны увидеть данные нашей модели в виде нового заголовка.  Замечательно! Хорошо, теперь, когда мы знаем, что у нас есть модель для использования, давайте удалим все, что мы захардкодили до этого! Вместо жесткого кодирования информации об аренде в наш компонент <Rental>, мы можем передать объект модели. Давайте попробуем. Во-первых, давайте передадим нашу модель нашему компоненту <Rental> в качестве аргумента @rental. Мы также удалим посторонний <h1>, который мы добавили ранее чтобы проверить, что все работает:  Передав @model в компонент <Rental> в качестве аргумента @rental, мы получим доступ к нашему объекту модели «Grand Old Mansion» в шаблоне компонента <Rental>! Теперь мы можем заменить наши жестко запрограммированные значения в этом компоненте, используя значения из нашей модели @rental. 
 Поскольку объект модели содержит те же данные, что и ранее жестко запрограммированный «Grand Old Mansion», страница должна выглядеть точно так же, как и до изменения.  Последнее, что нужно сделать: обновить тесты, чтобы отразить это изменение. Поскольку тесты компонентов предназначены для визуализации и тестирования одного компонента изолированно от остальной части приложения, они не выполняют никакой маршрутизации, что означает, что у нас не будет доступа к тем же данным, которые возвращаются из хука модели. Поэтому в тесте нашего <Rental> компонента нам нужно будет подать данные в него другим способом. Мы можем сделать это, используя setProperties, о котором мы узнали в предыдущей главе .  Обратите внимание, что нам также необходимо обновить вызов компонента <Rental> в вызове функции render чтобы он также получал данные через аргумент @rental. Если мы запустим наши тесты сейчас, они все должны пройти успешно!  Теперь, когда у нас есть все на своих местах, давайте самую приятную часть, удалив все значения, которые мы раньше захардкодили, из хука модели и на самом деле получим данные с сервера! В реальном приложении данные, которые мы запросим, скорее всего, будут поступать с удаленного сервера API. Но мы снова немного схитрим, чтобы избежать настройки сервера API только для этого туториала. Вместо этого мы поместим некоторые данные JSON в общую папку. Таким образом, мы будем запрашивать эти данные JSON с помощью регулярных HTTP-запросов — так же, как с реальным сервером API, — но без необходимости писать какую-либо серверную логику. Круто! Но откуда данные? Вы можете скачать этот файл данных, где мы подготовили некоторые данные в формате JSON и заархивировали их в файл формата .zip. Извлеките его содержимое в папку public. Когда вы закончите, ваша папка public должна теперь иметь следующий вид: Вы можете убедиться, что все работает правильно, отрыв в браузере URL http://localhost:4200/api/rentals.json.  Наш «сервер» теперь запущен и обслуживает наши арендные объекты в виде данных JSON. Прекрасно! Теперь давайте снова обратим наше внимание на наш хук модели. Нам нужно изменить это так, чтобы мы на самом деле запрашивали данные с сервера.  Что тут происходит? Прежде всего, мы используем Fetch API браузера для запроса данных JSON из API нашего сервера по адресу /api/rentals.json, тому же URL, что мы открывали в браузере ранее. Как упоминалось выше, выборка данных с сервера обычно является асинхронной операцией. Fetch API принимает это во внимание, поэтому fetch является async функцией, как и наш хук модели. Чтобы использовать его ответ, нам нужно воспользоваться ключевым словом await. Fetch API возвращает объект ответа асинхронно. Получив этот объект, мы можем преобразовать ответ сервера в любой нужный нам формат; в нашем случае мы знали, что сервер отправлял данные в формате JSON, поэтому мы можем использовать метод json() для соответствующей обработки данных ответа. Обработка данных ответа также является асинхронной операцией, поэтому здесь мы снова воспользуемся ключевым словом await. Прежде чем идти дальше, давайте на секунду остановимся, чтобы снова посмотреть на данные нашего сервера. 
 Эти данные соответствуют формату JSON:API, который немного отличается от жестко закодированных данных, которые мы возвращали из хука модели ранее. Во-первых, формат JSON:API возвращает массив, вложенный под ключом "data", а не только данные для одного арендуемого свойства. Это логично; Теперь мы хотим показать весь список арендуемых свойств, которые поступают с нашего сервера, а не только один, поэтому массив объектов арендуемой недвижимости — это то, что нам нужно. Объекты арендуемой недвижимости, содержащиеся в массиве, также имеют немного другую структуру. Каждый объект данных имеет type и id, которые мы не собираемся использовать в нашем шаблоне (пока!). На данный момент единственные данные, которые нам действительно нужны, вложены в ключ attributes. Здесь есть еще одно ключевое отличие, которое, возможно, смогут уловить только те, у кого очень острые глаза: в данных, поступающих с сервера, отсутствует свойство type, которое ранее существовало в нашем жестко-закодированном объекте модели. Свойство type может быть либо "Standalone" либо "Community", в зависимости от типа арендуемого имущества, который требуется для нашего компонента <Rental>. Во второй части этого руководства мы узнаем о более удобном способе использования данных в формате JSON:API. Сейчас мы можем немного трансформировать данные и самостоятельно разобраться с этими различиями в форматах. Мы можем справиться со всем этим в нашем хуке модели:  После обработки (parsing) данных JSON мы извлекли объект с вложенными attributes, вручную добавили атрибут отсутствующего type, а затем вернули его из хука модели. Таким образом, остальная часть нашего приложения даже не будет знать, что эта разница когда-либо существовала. Потрясающие! Теперь мы в деле. Последнее изменение, которое нам нужно сделать, — это изменить шаблон маршрута index.hbs, где мы вызываем наши компоненты <Rental>. Ранее мы передавали @rental как @model нашим компонентам. Тем не менее, @model больше не является единственным объектом — это массив (почти)! Итак, нам нужно изменить шаблон, чтобы учесть эту разницу. Посмотрим как.  Мы можем использовать синтаксис {{#each}}...{{/each}} для итерации и циклическому проходу по массиву, возвращаемому хуком модели. Для каждой итерации массива — для каждого элемента в массиве — мы будем визуализировать переданный ему блок. В нашем случае блок — это наш компонент <Rental>, окруженный тегами <li>. Внутри блока у нас есть доступ к элементу текущей итерации с помощью переменной {{rental}}. Но почему rental? Ну, потому что мы его так назвали! Эта переменная происходит от as |rental| при объявлении each цикла. Мы могли бы так же легко назвать её как-нибудь еще, например as |property|, в этом случае мы должны были бы получить доступ к текущему элементу через переменную {{property}}. Теперь давайте перейдем к нашему браузеру и посмотрим, как выглядит наш индексный маршрут с этим изменением.  Ура! Наконец, мы видим различные арендуемые объекты в нашем списке. И мы всего лишь немного поиграли с fetch и написали цикл. Довольно продуктивно, не правда ли? И что характерно, все наши тесты также проходят!  К этому моменту вы уже знаете основные моменты фреймворка и хорошо подготовлены для выполнения самых разнообразных задач по разработке в Ember! Сделайте перерыв или поэкспериментируйте с созданием собственного уникального приложения Ember, используя только что приобретенные навыки. Не стесняйтесь делиться успехами в твиттере используя хэштег #Emberjs. Еще можно посетить Discord канал сообщества, а также русскоязычный телеграмм канал ember_js Когда вы вернетесь, мы будем опираться на то, что узнали в первой части, и перейдем на новый уровень! UPDATE: И сноса благодарю пользователя MK1301 за коррекцию ошибок.