Building a Cardstack app - Part 2
08 December 2017
At the end of Part 1, we managed to set up and configure the Cardstack plugins we needed and were greeted with a "Welcome to Cardstack!" header as our reward.
Here, in Part 2, we'll add the rental listing on the main page and create the rental details page, learning how to use Cardstack components, search and all the code Cardstack generates for us in the process.
Listing rentals on the main page
The first feature we'll implement is listing the rentals on the main page and filtering them by city.
We established that the content on the main page is rendered by the cardstack/page-page
component and we currently only have the title of the page displayed:
We'll need to add the listing below it.
As I mentioned in Part 1, Cardstack is a search-first web framework so we're going to trigger a back-end search to fetch the rental items. But first, let's install the cardstack search add-on (and then restart):
1$ ember install @cardstack/search
Now, let's define the search parameters for the main page. We want to fetch all rentals on the main page, so let's add a mainQuery
property to the page to specify this. The properties of the main page are defined in the models.js
file, so let's edit that:
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', 'pages') 9 .withAttributes({ 10 'routing-field': 'permalink' 11 }) 12 .withRelated('fields', [ 13 factory.addResource('fields', 'title') 14 .withAttributes({ 15 'field-type': '@cardstack/core-types::string' 16 }), 17 factory.addResource('fields', 'body') 18 .withAttributes({ 19 'field-type': '@cardstack/core-types::string' 20 }), 21 factory.addResource('fields', 'permalink') 22 .withAttributes({ 23 'field-type': '@cardstack/core-types::string' 24 }), 25 factory.addResource('fields', 'main-query') 26 .withAttributes({ 27 'field-type': '@cardstack/core-types::object' 28 }), 29 ]); 30 31 (...) 32 33 factory.addResource('pages').withAttributes({ 34 permalink: " ", 35 title: "Welcome!", 36 mainQuery: { 37 type: 'rentals' 38 } 39 }); 40 41 return factory.getModels(); 42}
We added a main-query
field that is of type object, and set it to fetch all rentals on the main page, by only specifying the rentals
type, without any more parameters.
Integrating search results into pages
We should now add search results to the main page.
We do that by adding the cardstack-search
smart component which will trigger the search to the backend. In its simplest form, it takes a query object in its query
attribute that defines the search parameters.
1// app/templates/components/cardstack/page-page.hbs 2<h1>{{content.title}}</h1> 3 4{{#with content.mainQuery as |query|}} 5 {{#cardstack-search query=query as |item|}} 6 <article class="listing"> 7 <h3><a href={{cardstack-url item}} >{{item.title}}</a></h3> 8 </article> 9 {{/cardstack-search}} 10{{/with}}
Our search-enabled app reloads and duly sends a request to /api/rentals
which runs into a 404, because that endpoint doesn't exist, Cardstack doesn't know squat about the rentals type.
Now comes some dark magic.
Let's open the schema definition file again, and define a rental type.
1// cardstack/seeds/development/models.js 2const Factory = require('@cardstack/test-support/jsonapi-factory'); 3(...) 4 5function initialModels() { 6 (...) 7 factory.addResource('content-types', 'rentals') 8 .withRelated('fields', [ 9 factory.addResource('fields', 'title') 10 .withAttributes({ 11 'field-type': '@cardstack/core-types::string' 12 }), 13 factory.addResource('fields', 'owner') 14 .withAttributes({ 15 'field-type': '@cardstack/core-types::string' 16 }), 17 factory.addResource('fields', 'city') 18 .withAttributes({ 19 'field-type': '@cardstack/core-types::string' 20 }), 21 factory.addResource('fields', 'property-type') 22 .withAttributes({ 23 'field-type': '@cardstack/core-types::string' 24 }), 25 factory.addResource('fields', 'bedrooms') 26 .withAttributes({ 27 'field-type': '@cardstack/core-types::integer' 28 }), 29 factory.addResource('fields', 'description') 30 .withAttributes({ 31 'field-type': '@cardstack/core-types::string' 32 }) 33 ]); 34}
That is the class definition of the Rental type, now let's also add a handful of rentals.
1// cardstack/seeds/development/models.js 2const Factory = require('@cardstack/test-support/jsonapi-factory'); 3(...) 4 5function initialModels() { 6 (...) 7 8 factory.addResource('rentals').withAttributes({ 9 "title": "Grand Old Mansion", 10 "owner": "Veruca Salt", 11 "city": "San Francisco", 12 "property-type": "Estate", 13 "bedrooms": 15, 14 "description": "This grand old mansion sits on over 100 acres of rolling hills and dense redwood forests." 15 }); 16 17 factory.addResource('rentals').withAttributes({ 18 "title": "Urban Living", 19 "owner": "Mike Teavee", 20 "city": "Seattle", 21 "property-type": "Condo", 22 "bedrooms": 1, 23 "description": "A commuters dream. This rental is within walking distance of 2 bus stops and the Metro." 24 }); 25 26 factory.addResource('rentals').withAttributes({ 27 "title": "Downtown Charm", 28 "owner": "Violet Beauregarde", 29 "city": "Portland", 30 "property-type": "Apartment", 31 "bedrooms": 3, 32 "description": "Convenience is at your doorstep with this charming downtown rental. Great restaurants and active night life are within a few feet." 33 }); 34}
Every time we change the schema definition, we have to restart our app so that server-side operations can take place. When we load the main page of our app again, we see that it's actually working, the title of each listing is displayed:
Please notice (because it's awesome!) that we never implemented any API endpoints. By virtue of having defined the Rental type, Cardstack (more precisely, the @cardstack/jsonapi
package) generated those CRUD endpoints for us.
Displaying rental properties
As a next step, let's now display more properties for each rental in the listing. Let's do that by rendering a component for each search result item instead of writing the markup inline.
The cardstack-content
is the workhorse of UI customization in Cardstack. It takes a content
object for the object to be rendered and a format
property that defines how it should be rendered. The cardstack-content
component then goes on to render the component that is named cardstack/${type}-${format}
.
At any rate, when we save the above template, Cardstack will tell us which component we need to define by including its name in the error message on the page.
So let's create said component:
1$ ember g component cardstack/rental-listing
And move the few lines that we had in the each
loop to the component's template for each listing:
The only modification we had to make was to write content
instead of item
, because that's what the resource object is bound to inside cardstack-component.
With this change, we're back to having the title for each property listed. We can now add all the other information:
1<!-- app/templates/components/cardstack/rental-listing.hbs --> 2<article class="listing"> 3 <h3><a href={{cardstack-url content}} >{{content.title}}</a></h3> 4 <div class="detail owner"> 5 <span>Owner:</span> {{content.owner}} 6 </div> 7 <div class="detail type"> 8 <span>Type:</span> {{rental-property-type content.propertyType}} - {{content.propertyType}} 9 </div> 10 <div class="detail location"> 11 <span>Location:</span> {{content.city}} 12 </div> 13 <div class="detail bedrooms"> 14 <span>Number of bedrooms:</span> {{content.bedrooms}} 15 </div> 16 <div class="detail bedrooms"> 17 <span>Sleeps:</span> {{content.sleeps}} 18 </div> 19</article>
It all works, the rental list items now display all information correctly:
Extending generated classes
Taking a second look at our page, we notice that the 'Sleeps' property is not displayed correctly, it displays "Enter undefined" instead of the number of people the rental can sleep. Let's fix this next.
If we take a closer look at the template, we see that {{content.sleeps}}
is rendered, where content
is a rental model object. However, the Rental class (that we inherited from the original SuperRentals app) doesn't have a sleeps
property so it makes sense that its value is undefined.
Now, here is another thing that I really like about Cardstack.
Taking the model schema (that we defined in models.js), it automatically generates Ember Data classes for each type in our definition. The @cardstack/models
package has a code generator that accomplishes this and you can see these generated models if you open the app's vendor.js and search for 'models/generated'.
The SuperRentals app defined a Rental class so let's rewrite this by extending the generated model and defining the sleeps
computed property therein:
If we didn't need the sleeps
CP we wouldn't even have to have a file for the model. Cardstack takes the power of conventions even further and saves us from having to write boilerplate code for the models, too.
Rental details page
You might have noticed that we have a link for each rental in the list where the href is given by a call to cardstack-url
. I explained this helper in Part 1, explaining that it takes a resource type and an id to create a URL from.
Here, we use it differently, only passing in a resource object. Since that object has an id, the URL can be generated. For a rental object with an id of 24, the URL will be /rentals/24
.
Following one of the links, we transition to a URL like /rentals/1
and are greeted with the "No such component cardstack/rental-page" error message.
By now, we know really well what should be done to fix this.
Let's create a cardstack/rental-page
component and write in its template what we want to see on the rental (details) page. The content
property will be bound to the rental within the template:
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> </p> 21 <p class="description">{{content.description}}</p> 22 </div> 23</div>
We'll need to add a tiny css rule to make it look good:
Now our rental details page looks swell, too:
In the next part
Other than the "static" About and Contact pages, what's missing from the original SuperRentals app is the displaying of rental images, toggling between a wide and a normal image view and the city filter.
We'll add all of these in Part 3.
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!
Part 3 is now available.
Share on Twitter