Building a Cardstack app - Part 3

19 January 2018

We ended Part 2 with a working rental listing and rental details page. All information was on the page except for the image of the rental. So in this part, we're going to add that, make sure that we can switch between the wide and normal image variations and then implement the city filter for searching rentals.

Adding the image field

We start by adding an image field to the Rental model in our model definition.

 1// cardstack/seeds/development/models.js
 2const Factory = require('@cardstack/test-support/jsonapi-factory');
 3(...)
 4
 5function initialModels() {
 6  let factory = new Factory();
 7  (...)
 8  factory.addResource('content-types', 'rentals')
 9    .withRelated('fields', [
10      factory.addResource('fields', 'title')
11        .withAttributes({
12          'field-type': '@cardstack/core-types::string'
13        }),
14      (...)
15      factory.addResource('fields', 'image')
16        .withAttributes({
17          'field-type': '@cardstack/core-types::string'
18        }),
19  ]);
20}

We can now display the image in both the listing and the page format for rental cards. Let's start with the main page where each rental is displayed in its rental format.

We can copy over the markup used by the Super Rentals app for the image:

 1<!-- app/templates/components/cardstack/rental-listing.hbs -->
 2<article class="listing">
 3+  <a {{action (mut isWide) (not isWide)}} class="image {{if isWide "wide"}}">
 4+    <img src={{content.image}} alt="">
 5+    <small>View Larger</small>
 6+  </a>
 7  <h3><a href={{cardstack-url content}} >{{content.title}}</a></h3>
 8  <div class="detail owner">
 9    <span>Owner:</span> {{content.owner}}
10  </div>
11  <div class="detail type">
12    <span>Type:</span> {{rental-property-type content.propertyType}} - {{content.propertyType}}
13  </div>
14  <div class="detail location">
15    <span>Location:</span> {{content.city}}
16  </div>
17  <div class="detail bedrooms">
18    <span>Number of bedrooms:</span> {{content.bedrooms}}
19  </div>
20  <div class="detail bedrooms">
21    <span>Sleeps:</span> {{content.sleeps}}
22  </div>
23</article>

We only made two small changes in the new snippet. Instead of handling the click with an explicit toggleImageSize action, we implemented it in the template, to avoid having to create a component just for this reason. The other change is the usual one: inside cardstack components, the "model" object passed in is bound to the content property, so content.image needs to be written.

After an app restart (remember, we changed the model schema), the images now show up:

Listing with images

Oh, while we're at it, let's add the little map to each location listing. There's nothing cardstacky about this step, so I'll skip any explanations:

1<!-- app/templates/components/cardstack/rental-listing.hbs -->
2<article class="listing">
3  (...)
4  {{location-map location=content.city}}
5</article>

Listing with images and map

Showing the image on rental details page is even simpler, it only takes one extra line:

 1<!-- app/templates/components/cardstack/rental-page.hbs -->
 2<div class="jumbo show-listing">
 3  <h2 class="title">{{content.title}}</h2>
 4  <div class="right detail-section">
 5    <div class="detail owner">
 6      <strong>Owner:</strong> {{content.owner}}
 7    </div>
 8    <div class="detail">
 9      <strong>Type:</strong> {{rental-property-type content.propertyType}} - {{content.propertyType}}
10    </div>
11    <div class="detail">
12      <strong>Location:</strong> {{content.city}}
13    </div>
14    <div class="detail">
15      <strong>Number of bedrooms:</strong> {{content.bedrooms}}
16    </div>
17    <div class="detail bedrooms">
18      <strong>Sleeps:</strong> {{content.sleeps}}
19    </div>
20    <p>&nbsp;</p>
21    <p class="description">{{content.description}}</p>
22  </div>
23+  <img src={{content.image}} class="rental-pic" />
24</div>

Rental details page with image

Filtering by city

You might notice that our Cardstack app is still missing the ability to filter by city name, so let's add it.

The pure-Ember solution uses a list-filter component which yields its results to the wrapped element (and uses store.query behind the scenes). We already have the cardstack-search component that does this, so we just have to add the input and create an action handler that updates the mainQuery property of the main page.

First, let's add the input:

 1<!-- app/templates/components/cardstack/page-page.hbs -->
 2<h1>{{content.title}}</h1>
 3
 4+ <div class="list-filter">
 5+   {{input value=cityFilter key-up=(action 'updateQuery') class="light" placeholder="Filter By City"}}
 6+ </div>
 7
 8{{#with content.mainQuery as |query|}}
 9  {{#cardstack-search query=query as |item|}}
10    {{cardstack-content content=item format="listing"}}
11+  {{else}}
12+    <p class="empty-list-blurb">There are no rentals that match the search criteria.</p>
13  {{/cardstack-search}}
14{{/with}}

We defined the updateQuery action for updating the query when the input changes its value but it doesn't exist yet so we'd receive an error. Let's proactively define the action. We'll first have to create the component file itself as we haven't needed it yet:

 1// app/components/cardstack/page-page.js
 2import Component from '@ember/component';
 3
 4export default Component.extend({
 5  actions: {
 6    updateQuery() {
 7      let query = { type: 'rentals' };
 8      let city = this.get('cityFilter');
 9      if (city) {
10        query.filter = {
11          city: {
12            prefix: city
13          }
14        }
15      }
16      this.set('content.mainQuery', query);
17    }
18  }
19});

So if the input, and thus cityFilter, has a non-empty value, we'll update the query with a filter for the city field. The filter parameter is part of JSON API and is fully supported by Cardstack endpoints, so we expect the above to "just work" as it is.

We set the query to be a prefix one, so that if the user only types 's', both the Seattle and the San Francisco rentals should be returned. (As the underlying search engine is Elastic Search, we could choose to have a different type of search like an exact or partial one.)

After the app reloads and we enter a search term, we can see that the right query param is appended to the request,

Rental query in DevTools

and the cardstack-search component indeed correctly returns the rentals that have a matching city name:

Rental listing with city filter

In the next part

We have now reached feature parity with the original Super Rentals app. In the next part(s), we'll enhance the app with CMS features, so that we can create rentals and edit their details.

If you're interested in learning more about Cardstack, or contributing to it, check out the official Cardstack site and the source code repository. If you have any questions, hop into our chat and ask away!

Share on Twitter