# Reusable content with pieces
When you create content with the rich text editor, or images widget, that content is only displayable on the page where it is created. That will work for some cases, but often you need more powerful tools to create different types of content that can be reused in different ways across your site.
# Creating a directory of people with "pieces"
Let's say you want to create a directory of people who work for a company.
"People" are a general type of content. They are useful to display in multiple
places all over the site and they aren't limited to one page. The
apostrophe-pieces
module provides a great starting point to create many such
content types. You'll extend it to make your own people
module. You can extend
apostrophe-pieces
many times in the same project.
TIP
You can use the Apostrophe CLI. To
generate the initial pieces module structure. You could skip a few steps below
if you have it installed by running apos add piece person
(see the note in
the link above about pluralization). The following information is still worth
reviewing to understand what this does for you.
- Create a
lib/modules/people/index.js
file:
module.exports = {
extend: 'apostrophe-pieces',
name: 'person',
label: 'Person',
pluralLabel: 'People',
addFields: [
{
name: 'title',
label: 'Full Name',
type: 'string',
required: true
},
{
name: 'firstName',
label: 'First Name',
type: 'string',
required: true
},
{
name: 'lastName',
label: 'Last Name',
type: 'string',
required: true
},
{
name: 'body',
label: 'Biography',
type: 'area',
options: {
widgets: {
'apostrophe-rich-text': {
toolbar: [ 'Bold', 'Italic', 'Link', 'Unlink' ]
},
'apostrophe-images': {}
}
}
},
{
name: 'phone',
label: 'Phone',
type: 'string'
},
{
name: 'thumbnail',
label: 'Thumbnail',
type: 'singleton',
widgetType: 'apostrophe-images',
options: {
limit: 1,
minSize: [ 200, 200 ],
aspectRatio: [ 1, 1 ]
}
}
]
};
NOTE
IMPORTANT: note the name
property. This identifies ONE piece in the database, so it is always singular. Remember: Modules Are Plural (MAP), but the things they manage may not be.
- Now turn the module on in
app.js
.
modules: {
// other modules ...
'people': {}
}
NOTE
When code examples show sections like the modules
section in app.js
we're only going to show you the relevant portion that you're currently working on. In many cases you'll have a lot more in those sections, but we don't need to replicate that every time.
Just like that, you'll see a new "People" menu in the admin bar when you log into the site. Pick "New Person" and you'll find that you can give each person a full name, a first name, a last name and a phone number. Pick "Manage Person" to examine and edit existing people.
TIP
You could have configured the module entirely in app.js
. But that leads to giant app.js
files, so we don't recommend it. However, some developers feel it's a good place for high-level properties like extend
that help give you a quick overview of what the module is and does.
There are certain additional fields that you get by default with every piece, such as title
(the full name of the piece), slug
(used when the piece appears as part of a URL), and published
(which determines whether the public can see the piece, as you'll see below). But in this case, you re-declared title
in order to change its label to Full Name
so that the "New Person" form is not confusing.
You can even add a profile photo, via the thumbnail
field. This field has the singleton
type, which allows you to include a widget in the schema for this type of piece, exactly as if you were calling apos.singleton
in a template. You just need to specify the widgetType
and pass any desired options to the widget via the options
property. You can also add fields of the area
type.
And, there's a "biography" section. This is a full-blown content area in which the editor can add rich text and images. There's nothing to stop us from allowing more controls and widgets here. Limiting the choices just helps keep things from getting out of hand.
# Customizing the model layer: setting the title
automatically
Right now, the title
property (which is always the full name of the piece) is independent of firstName
and lastName
. In this example, it makes more sense for the title
to be generated automatically from firstName
and lastName
.
To do this, add this beforeSave
method in lib/modules/people/index.js
:
module.exports = {
// Same configuration as before, then...
construct: function(self, options) {
self.beforeSave = function(req, piece, options, callback) {
piece.title = piece.firstName + ' ' + piece.lastName;
return callback();
};
}
};
Now the title
property is set automatically from the first and last name.
TIP
Methods are always added to the module in the construct
function, which takes the module object, self
, as its first argument. You attach methods directly to self
. The use of self
rather than this
ensures that methods can make asynchronous calls and be passed as callbacks themselves without any confusion about the value of this
. For more information about object-oriented functional programming in Apostrophe, check out moog (opens new window) and moog-require (opens new window).
# Using Contextual Fields to Simplify the Form
When you do this, there is still a separate prompt to enter the full name. Remove that by adding the contextual
option to the title
field, which keeps that field out of the modal:
// In the`addFields` array
{
name: 'title',
label: 'Full Name',
type: 'string',
required: true,
contextual: true
}
]
TIP
There are many other methods you can override or extend to change the behavior of pieces. See the apostrophe-pieces API methods for more information.
# Arranging fields
As you create increasingly complex schemas for pieces and widgets, you want to arrange the fields in the modal in a way that supports a logical workflow for editors.
You can use arrangeFields
to break the schema into multiple tabs in the editor modal. This can be achieved by passing an array of objects, each containing a name, label, and array of fields, to arrangeFields
:
// lib/modules/people/index.js
module.exports = {
addFields: [ ... ],
arrangeFields: [
{
name: 'contact',
label: 'Contact',
fields: [ 'firstName', 'lastName', 'phone' ]
},
{
name: 'admin',
label: 'Administrative',
fields: [ 'slug', 'published', 'tags' ]
},
{
name: 'content',
label: 'Biographical',
fields: [ 'thumbnail', 'body' ]
}
],
// ...
}
Any non-contextual
fields excluded from this configuration will be placed in an additional tab.