You are looking at the old Apostrophe 0.5 documentation. It is deprecated for new projects. Check out the latest version!

Creating Your Own Content Types: Subclassing Snippets (The Basics)

← Previous: Using Snippets Directly

It's possible to create your own content types based on snippets. This has many advantages. All of the tools to manage snippets have already been built for you and are easily extended without code duplication to accommodate new content. The snippets module also implements an Apostrophe page loader function for you, ready to display "index pages" and "show pages" out of the box. And of course a widget for reusing snippets anywhere on the site is built in. All of this functionality is easily obtained for your new content type as well.

Absolutely nothing is preventing you from implementing your own page loader functions and your own admin tools for managing content, and sometimes this may be desirable. But in most cases subclassing snippets is the right way to go.

Subclasses of snippets can extend their behavior on both the server side and the browser side. Most of the job can be done simply through configuration in app.js, but you may need to extend the code on the server side as well to add custom features. And extra browser-side code is also desirable at times. We'll see below how to do both.

The apostrophe-blog, apostrophe-events and apostrophe-map modules are all simple subclasses of apostrophe-snippets and they make good examples if you wish to learn how to package your work as an npm module for the benefit of the community.

Configuring New Content Types

You can create a new content type just by configuring it in app.js along with other modules. Let's invent a new content type called "stories":

modules: {
  ... other modules ...
  'stories': {
    extend: 'apostrophe-snippets',
    name: 'stories',
    label: 'Stories',
    instance: 'story',
    instanceLabel: 'Story',
    addFields: [
      {
        name: 'year',
        type: 'integer',
        label: 'Year',
        def: '2013'
      },
      {
        name: 'publisher',
        type: 'string',
        label: 'Publisher',
      }
    ]
  }
}

The extend property tells Apostrophe what module you're subclassing. You can subclass apostrophe-blog or apostrophe-events instead if they are closer to what you need.

The instance property is a singular word for one item - one story, in this case. name is a name for data type as a whole and is usually plural (like "snippets" or "events" or "blog"). label and instanceLabel are publicly visible versions of these and should be capitalized.

addFields allows us to add new fields to our content type. We'll examine it in more detail below.

You must also create lib/modules/stories in your project. Soon we'll add custom templates there, but it must exist even before you do that.

Edit outerLayout.html and add a line to insert the menu for managing stories:

  {{ aposStoryMenu({ edit: permissions.admin }) }}

And... that's actually enough to get started! With just this much code, you can already create, edit and manage stories, including the custom fields year and publisher. All the plumbing is automatic. Nice, yes?

Custom Templates

Your code automatically inherits its templates from the snippets module. But the bare-bones templates we supply for the index and show views of snippets are not very exciting. So, create your own! Just copy those templates to lib/modules/stories/views/index.html and lib/modules/stories/views/show.html and modify them as you see fit.

We recommend creating your own, additional storyMacros.html file and including it in your templates. Don't override snippetMacros.html in your module. We frequently improve that file and you don't want to lose access to those improvements.

Adding New Properties To Your Snippets Using the Schema

There is a very easy way to do this. Snippets now support a simple JSON format for creating a schema of fields. Both the browser side and the server side understand this, so all you have to do is add them to the dialogs as described below and set up the schema. You can still do it the hard way, however, if you need custom behavior.

Here is a super-simple example of a project-level subclass of the people module (itself a subclass of snippets) that adds new fields painlessly. Here I assume you are using apostrophe-site to configure your site (you should be).

... Configuring other modules ...
'apostrophe-people': {
  addFields: [
    {
      name: 'workPhone',
      type: 'string',
      label: 'Work Phone'
    },
    {
      name: 'workFax',
      type: 'string',
      label: 'Work Fax'
    },
    {
      name: 'department',
      type: 'string',
      label: 'Department'
    },
    {
      name: 'isRetired',
      type: 'boolean',
      label: 'Is Retired'
    },
    {
      name: 'isGraduate',
      type: 'boolean',
      label: 'Is Graduate'
    },
    {
      name: 'classOf',
      type: 'string',
      label: 'Class Of'
    },
    {
      name: 'location',
      type: 'string',
      label: 'Location'
    }
  ]
}, ... more modules ...

What Field Types Are Available?

Currently:

string, boolean, integer, float, select, checkboxes, url, date, time, slug, tags, password, area, singleton, `array

Except for area and array, all of these types accept adef` option which provides a default value if the field's value is not specified.

The string type accepts the textarea option, which causes the input to appear as a textarea in the new and edit menus when set to true.

The integer and float types also accept min and max options and automatically clamp values to stay in that range.

The select and checkboxes types accept a choices option which should contain an array of objects with value and label properties. An optional showFields property in each choice object is an array of field names you wish to make visible only when this choice is selected (they are hidden when the choice is not selected). See conditional fields for more information.

The date type pops up a jQuery UI datepicker when clicked on, and the time type tolerates many different ways of entering the time, like "1pm" or "1:00pm" and "13:00".

The url field type is tolerant of mistakes like leaving off http:.

The password field type stores a salted hash of the password via apos.hashPassword which can be checked later with the password-hash module. If the user enters nothing the existing password is not updated.

When using the area and singleton types, you may include an options property which will be passed to that area or singleton exactly as if you were passing it to aposArea or aposSingleton.

When using the singleton type, you must always specify widgetType to indicate what type of widget should appear.

arrays allow you to add one or more items which have their own schema. See arrays in schemas for more information.

Joins with other snippet and page types are also supported.

Removing Fields

Two fields come standard with snippets: thumbnail and body. thumbnail is a singleton with widget type slideshow, and body is an area.

If either of these is of no use to you, just remove it:

'my-own-thing': {
  removeFields: [ 'thumbnail', 'body' ]
}

Changing the Order of Fields

When adding fields, you can specify where you want them to appear relative to existing fields via the before, after, start and end options:

addFields: [
  {
    name: 'favoriteCookie',
    type: 'string',
    label: 'Favorite Cookie',
    after: 'title'
  }
]

Any additional fields after favoriteCookie will be inserted with it, following the title field.

Use the before option instead of after to cause a field to appear before another field.

Use start: true to cause a field to appear at the top.

Use start: end to cause a field to appear at the end.

If this is not enough, you can explicitly change the order of the fields with orderFields:

'apostrophe-people': {
  orderFields: [ 'year', 'specialness' ]
}

Any fields you do not specify will appear in the original order, after the last field you do specify (use removeFields if you want a field to go away).

Making Fields Conditional

You can use a select field to create sets of conditional fields in your snippet. For example, your project may have two kinds of people, "participants" and "coordinators." You want the "birthdate" field to be visible only for "participants," and you want the "job title" field to be visible only for coordinators.

Keep in mind that if your sets of fields are dramatically different, they should be two different subclasses of snippets, not conditional fields. Conditional fields are intended for cases like this one in which there is still some common ground.

To make fields conditional, all you need to do is add them to the showFields property of a choice array on a select field:

'apostrophe-events': {
  addFields: [
    {
      name: 'host',
      label: 'Host',
      type: 'string'
    },
    {
      name: 'dj',
      label: 'DJ',
      type: 'string'
    },
    {
      name: 'bands',
      label: 'Bands Playing',
      type: 'string'
    },
    {
      name: 'eventType',
      label: 'Event Type',
      type: 'select',
      choices: [
        {
          value: 'party',
          label: 'Party',
          showFields: ['host', 'dj']
        },
        {
          value: 'concert',
          label: 'Concert',
          showFields: ['bands']
        }
      ]
    }
  ]
}

This will toggle between showing the party fields (host and dj) and the concert fields (bands) depending on the value of the eventType select field. You can also specify showFields in only one of the choices to show those fields only when that choice is selected, and hide them otherwise.

You may use required: true in conjunction with this feature as of apostrophe-schemas version 0.5.80. If a conditional field is required, its value will be required and saved if and only if the relevant select choice is made. Previously, requiring a conditional field prevented you from saving the form at all if the field was empty and not currently active.

Note that this feature works not just on snippets, but anything that uses apostrophe-schemas to display a form.

Altering Fields: The Easy Way

It's easy to replace a field that already exists, such as the "body" field, for instance in order to change its type. Just pass it to addFields with the same name as the existing field:

'my-own-thing': {
  addFields: [
    {
      name: 'body',
      type: 'string',
      label: 'Body'
    }
  ]
}

Altering Fields: The Hard Way

There is also an alterFields option available. This must be a function which receives the fields array as its argument and modifies it. Most of the time you will not need this option; see removeFields, addFields and orderFields. It is mostly useful if you want to make one small change to a field that is already rather complicated. Note you must modify the existing array of fields in place.

Adding Properties to the New and Edit Dialogs

This is not your problem! The latest versions of the new.html and edit.html templates invoke snippetAllFields, a macro which outputs all of the fields in your schema, in order.

However, if you want to, or you need to because you are implementing extra fields without using the schema, then you can copy new.html to lib/modules/modulename/views/new.html. Since your template starts by extending the newBase.html template, you can be selective and just override the insideForm block to do something a little different with the fields, but not rewrite the entire template:

{% block insideForm %}
{{ snippetAllFields(fields, { before: 'shoeSize' }) }}
<p>Here comes the shoe size kids!</p>
{{ snippetText('shoeSize', 'Shoe Size') }}
<p>Wasn't that great?</p>
{{ snippetAllFields(fields, { from: 'shoeSize' }) }}
{% endblock %}

See snippetMacros.html for all the macros available to render different types of fields.

This example code outputs most of the fields in a long schema, then outputs one field directly, then outputs the rest of the fields.

In addition to before and from, you may also use after and to. before and after are exclusive, while from and to are inclusive. Combining before and from let us wrap something around a specific field without messing up other fields or even having to know what they are.

Of course you can also override new.html completely from scratch, provided you produce markup with the same data attributes and field names.

You usually won't need to touch edit.html because it gracefully extends whatever you do in new.html.

Note that the name of each property must match the name you gave it in the schema. weLikeMongoDb, soPleaseUseIntercap, not-hyphens_or_underscores.

Note that you do not need to supply any arguments that can be inferred from the schema, such as the choices list for a select property, or the widget type of a singleton. The real initialization work happens in browser-side JavaScript powered by the schema.

Search and Schema Fields

By default, all schema fields of type string, select, area and (in certain cases) singleton are included in the search index. You can shut this off by setting the search option to false for a particular field. You can also reduce the search index weight of the field by setting weight to a lower value. The built-in search engine prioritizes results with a weight greater than 10 over "plain old rich text." By default the weight for schema fields is 15.

Actually displaying your field as part of the summary shown when a snippet qualifies as a search result is usually not desirable, so by default this is not done. However you can include it in the summary text by setting the silent option to false.

Custom Field Types

You can define custom field types to be included in schemas. For this advanced topic, see the apostrophe-schemas documentation. The apostrophe-snippets module is based upon apostrophe-schemas, so everything that can be done there is also supported with snippets.

Next: Arrays in schemas →