Building a Cardstack app - Part 1

24 November 2017

What is Cardstack?

Cardstack is an open-source application framework, built on top of Ember.js and Node, that aims to make common application features (CMS, notifications, authentication) a snap to add to your application and empowering the end-user to do so.

It is also special in that it also provides a server-side component as part of the framework, so the front- and back-end can really work in tandem.

The Cardstack team has a grand and worthy vision for the future of software and I strongly encourage you to read the whitepaper (and I dare you not to get enthusiastic about the project once you’ve done so).

In this series of blog posts, I’m going to show you how to get started with Cardstack by leading you through the building of a simple app.

The tutorial app

We’re going to integrate Cardstack in the “Super Rentals” app developed in the official Ember.js tutorial. It’s a really small app but it’s big enough to highlight some of the features Cardstack gives you.

Unsurprisingly, it’s built around rentals, and its main page shows a list of rentals that can be filtered by city.

Each rental can then be examined on its own page. There is also a simple About us and Contact page.

First part: Welcome to Cardstack

In this first installment of the tutorial, we’ll see how to integrate Cardstack into this app until the point where it displays the main page with a welcome message.

We’re going to do a lot of groundwork, things that don’t have an immediate, user-facing result, but which will allow us to build really fast and high later.

Let’s jump in.

Setting up the app

If you’d like to follow along, which I encourage you to do, you first have to check out the super-rentals repo:

1
$ git clone https://github.com/ember-learn/super-rentals.git

We’ll use yarn as our package manager, so you’ll have to install it if you haven’t already done so. To be able to run the app, you’ll want to have a recent version of node, 8 is best.

Change to the folder you have cloned the repo to and install the project’s dependencies:

1
$ yarn

When you have this, start the app by launching ember serve. When you go to http://localhost:4200, you should see the list of rentals:

Super rentals main page

Setting up CardStack dependencies

Cardstack is a search-first web application and is powered by Elasticsearch. To make it easy for developers to set up Elasticsearch (and to prevent a slew of incompatibility and configuration issues), Cardstack also published a Docker repository you can download.

(If you don’t yet have Docker on your machine, you’ll first have to install it).

You can get the elasticsearch repository by issuing:

1
$ docker pull cardstack/elasticsearch:dev

And then running it using the following command:

1
$ docker run -d -p 9200:9200 --rm cardstack/elasticsearch:dev

Routing

If we peek into app/router.js, we’ll find a handful of routes:

1
2
3
4
5
6
7
8
9
10
(...)
Router.map(function() {
  this.route('about');
  this.route('contact');
  this.route('rentals', function() {
    this.route('show', { path: '/:rental_id' });
  });
});

export default Router;

To have these routes managed by Cardstack, we first need to add the @cardstack/routing package:

1
$ yarn add @cardstack/routing

Let’s go back to the router, remove the existing routes and extend it with the Cardstack ones:

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/router.js
import EmberRouter from '@ember/routing/router';
import config from './config/environment';
import { cardstackRoutes } from '@cardstack/routing';

const Router = EmberRouter.extend({
  location: config.locationType,
  rootURL: config.rootURL
});

Router.map(cardstackRoutes);

export default Router;

cardstackRoutes will contain the routes auto-generated by the framework for the models we’re about to define.

The all-important data schema

When launching the application with ember serve we get the following error:

1
2
Unable to load models from your seed-config-file (undefined),
Error: ENOENT: no such file or directory, scandir '.../super-rentals/cardstack/seeds/development'

Now that we have the hub, the main server-side piece of the hub architecture, we need to define the models there. Cardstack will then create models, routes and server endpoints(!), the full monty, based on the schema defined in the host application.

Let’s create the directory structure cardstack/seeds/development from our project folder:

1
$ mkdir -p cardstack/seeds/development

And then create a models.js file in that folder to define our data schema:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// cardstack/seeds/development/models.js

const Factory = require('@cardstack/test-support/jsonapi-factory');

module.exports = [
  {
    type: 'data-sources',
    id: 'default',
    attributes: {
      'source-type': '@cardstack/ephemeral',
      params: {
        initialModels: initialModels()
      }
    }
  },
  {
    type: 'plugin-configs',
    id: '@cardstack/hub',
    relationships: {
      'default-data-source': {
        data: { type: 'data-sources', id: 'default' }
      }
    }
  }
];

function initialModels() {
  let factory = new Factory();

  factory.addResource('content-types', 'pages')
    .withAttributes({
      'routing-field': 'permalink'
    })
    .withRelated('fields', [
      factory.addResource('fields', 'title')
        .withAttributes({
          'field-type': '@cardstack/core-types::string'
        }),
      factory.addResource('fields', 'body')
        .withAttributes({
          'field-type': '@cardstack/core-types::string'
        }),
      factory.addResource('fields', 'permalink')
        .withAttributes({
          'field-type': '@cardstack/core-types::string'
        }),
    ]);

  return factory.getModels();
}

Don’t worry about the module.exports part too much, the only thing we care about now is that it calls the initialModels function for the data source which should return the model definition.

Since we want to build a little CMS and work with pages, the first model we define therein should be page. Cardstack uses the JSON API format for defining data schema and the jsonapi-factory provides methods, like withAttributes and withRelated, to help create the documents that describe that data.

The page content type (the plural form is used in JSON API) has a routing-field with a value of permalink, which is the property used by the routing package to create and traverse URLs.

It also has fields, which is defined as a relation to the built-in fields content-type. Then come the fields themselves, each having an id (like title, body, etc.) and a field-type.

Good, so we’ve now defined the page schema (the Page class, if you will), we can now create page instances.

For now it suffices to have a main page, so let’s create one in the same file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// cardstack/seeds/development/models.js

const Factory = require('@cardstack/test-support/jsonapi-factory');

module.exports = [
  (...)
];

function initialModels() {
  (...)

  factory.addResource('pages').withAttributes({
    permalink: " ", // a string with a single space
    title: "Welcome to Cardstack!",
  });

  return factory.getModels();
}

Before we go on, let’s add the @cardstack/test-support package to our dependencies as we’re importing the jsonapi-factory module from there:

1
$ yarn add @cardstack/test-support

The next error we receive is:

1
> cardstack.index @cardstack/routing tried to use model page but it does not exist

We defined our data schema but for Cardstack to generate client-side models for it, we also need the @cardstack/models package, so let’s install it:

1
$ yarn add @cardstack/models

After a restart, we now get another error, but not to despair, keep in mind that receiving a different error each time is progress:

1
Mirage: Your Ember app tried to GET '/cardstack/api/pages?branch=master&filter[permalink][exact]= &page[size]=1',

Oh, ok. The Super Rentals app uses Mirage for mocking server responses, but Cardstack has a built-in server side that implements the CRUD actions based on our model schema, so we don’t need Mirage.

We have to replace Mirage with the @cardstack/jsonapi plugin:

1
2
$ yarn remove ember-cli-mirage
$ yarn add @cardstack/jsonapi

With that change, we again got closer to have a working, Cardstack-enabled landing page. The page header is rendered but when it gets to the header link, it bows out:

1
You attempted to define a `{{link-to "index"}}` but (...). There is no route named index"

Since we took over routing by way of the @cardstack/routing package, we have to use routes generated by it and the helpers it provides. In the header, we want to link to the main page that we created in the models seed file and we achieve it by using the cardstack-url helper (provided by @cardstack/routing) which takes a type and an id.

Let’s open the application template and replace the first link-to, while temporarily removing the links to the about and contact page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- app/templates/application.hbs -->
<div class="container">
  <div class="menu">
    <a href={{cardstack-url 'page' ' '}}>
      <h1>
        <em>SuperRentals</em>
      </h1>
    </a>
    <div class="links">
     <!-- we removed the other links from here -->
    </div>
  </div>
  <div class="body">
    {{outlet}}
  </div>
</div>

The header is now rendered and the body contains the error message: “No such component cardstack/page-page”.

No cardstack/page-page component

Great, we now we got to the core customization feature of Cardstack, components.

Cardstack components

Cardstack components is the main way of making your app look and behave the way you want it to while still having all the features the framework provides for you.

The naming rule for which component is rendered for a certain type of resources is

1
`cardstack/${resource-type}-${format}`

In our case, we have a resource of type page as the main page of the app. Since this object represents the whole page, Cardstack, by convention, tries to render a “page” object in “page” format, and that’s why it complains about not finding a cardstack/page-page component.

This might be confusing, so let me give you another example. If you have a resource of type rental, and a ‘card’ format, Cardstack will render the cardstack/rental-card component.

Rendering the main page

Ok, so we established we’d have to define a cardstack/page-page component to put some content on the main page. We already have a title property for pages so let’s see if we can make it appear.

We’ll create a template file for the component manually as we don’t need the javascript file, and put the following content into it:

1
2
// app/templates/components/cardstack/page-page.hbs
<h1>{{content.title}}</h1>

The content property in Cardstack components refers to the resource (in this case, the page).

The app reloads and our welcome message appears!

Welcome message appears

What did we accomplish?

We started with the simple Super Rentals app, and we added a few necessary Cardstack packages to enable easily adding features to it, which we’re going to do in the next part of the series.

We laid down the groundwork, and now construction can begin.

In the meantime, you can check out the official Cardstack site, follow its development, or ask us any questions you might have in our Slack group.

See you next time!


Part 2 is now available:

Building a Cardstack app - Part 2