UNPKG

40.4 kBMarkdownView Raw
1# apostrophe-schemas
2
3**Table of Contents**
4 * [Accessing the Schemas Object In Your Module](#accessing-the-schemas-object-in-your-module)
5 * [Adding New Properties To Objects Using the Schema](#adding-new-properties-to-your-snippets-using-the-schema)
6 * [What field types are available?](#what-field-types-are-available)
7 * [Validation](#validation)
8 * [Grouping fields into tabs](#grouping-fields-into-tabs)
9 * [Arrays in schemas](#arrays-in-schemas)
10 * Editing
11 * [Schemas in Nunjucks Templates](#editing-schemas-in-nunjucks-templates)
12 * [Browser-Side JavaScript](#editing-browser-side-javascript)
13 * [Saving Objects On the Server](#editing-saving-objects-on-the-server)
14 * [Editing Fields In Context](#editing-fields-in-context)
15 * [Joins in Schemas](#joins-in-schemas)
16 * [one-to-one](#one-to-one-joins)
17 * [reverse](#reverse-joins)
18 * [nested joins](#nested-joins-you-gotta-be-explicit)
19 * [many-to-many](#many-to-many-joins)
20 * [reverse many-to-many](#reverse-many-to-many-joins)
21 * [Complicated Relationships](#when-relationships-get-complicated)
22 * [Accessing Relationship Properties in a Reverse Join](#accessing-relationship-properties-in-a-reverse-join)
23 * [Overriding Templates For Individual Fields](#overriding-templates-for-individual-fields)
24 * [Adding New Field Types](#adding-new-field-types)
25 * Support for Subclassing and Mixins
26 * [Creating Schemas With Compose](#creating-schemas-with-compose)
27 * [Refining existing schemas with refine](#refining-existing-schemas-with-refine)
28 * [Filtering existing schemas with subset](#filtering-existing-schemas-with-subset)
29
30`apostrophe-schemas` adds support for simple schemas of editable properties to any object. Schema types include text, select, apostrophe areas and singletons, joins (relationships to other objects), and more. This module is used by the `apostrophe-snippets` module to implement its edit views and can also be used elsewhere.
31
32### Accessing the Schemas Object In Your Module
33
34In any project built with the [apostrophe-site](http://github.com/punkave/apostrophe-site) module, every module you configure in `app.js` will receive a `schemas` option, which is a ready-to-rock instance of the `apostrophe-schemas` module. You might want to add it as a property in your constructor:
35
36 self._schemas = options.schemas;
37
38**If you are not using `apostrophe-site`... well, you should be.** But a reasonable alternative is to configure `apostrophe-schemas` yourself in `app.js`:
39
40 var schemas = require('apostrophe-schemas')({ app: app, apos: apos });
41 schemas.setPages(pages); // pages module must be injected
42
43And then pass it as the `schemas` option to every module that will use it. But this is silly. Use `apostrophe-site`.
44
45### Adding New Properties To Objects Using the Schema
46
47A schema is a simple array of objects specifying information about each field. The `apostrophe-schemas` module provides methods to build schemas, validate submitted data according to a schema, and carry out joins according to a schema. The module also provides browser-side JavaScript and Nunjucks templates to edit an object based on its schema.
48
49Schema objects have intentionally been kept simple so that they can be send to the browser as JSON and interpreted by browser-side JavaScript as well.
50
51The simplest way to create a schema is to just make an array yourself:
52
53```javascript
54var schema = [
55 {
56 name: 'workPhone',
57 type: 'string',
58 label: 'Work Phone'
59 },
60 {
61 name: 'workFax',
62 type: 'string',
63 label: 'Work Fax'
64 },
65 {
66 name: 'department',
67 type: 'string',
68 label: 'Department'
69 },
70 {
71 name: 'isRetired',
72 type: 'boolean',
73 label: 'Is Retired'
74 },
75 {
76 name: 'isGraduate',
77 type: 'boolean',
78 label: 'Is Graduate'
79 },
80 {
81 name: 'classOf',
82 type: 'string',
83 label: 'Class Of'
84 },
85 {
86 name: 'location',
87 type: 'string',
88 label: 'Location'
89 }
90 ]
91});
92```
93
94However, if you are implementing a subclass and need to make changes to the schema of the superclass it'll be easier for you if the superclass uses the `schemas.compose` method, as described later.
95
96#### What Field Types Are Available?
97
98Currently:
99
100`string`, `boolean`, `integer`, `float`, `select`, `url`, `date`, `time`, `slug`, `tags`, `password`, `area`, `singleton`
101
102Except for `area`, all of these types accept a `def` option which provides a default value if the field's value is not specified.
103
104The `integer` and `float` types also accept `min` and `max` options and automatically clamp values to stay in that range.
105
106The `select` type accepts a `choices` option which should contain an array of objects with `value` and `label` properties. In addition to `value` and `label`, each choice option can include a `showFields` option, which can be used to toggle visibility of other fields when being edited.
107
108The `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".
109
110The `url` field type is tolerant of mistakes like leaving off `http:`.
111
112The `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.
113
114The `tags` option accepts a `limit` property which can be used to restrict the number of tags that can be added.
115
116When 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`.
117
118When using the `singleton` type, you must always specify `widgetType` to indicate what type of widget should appear.
119
120[Joins](#joins-in-schemas) and [arrays](#arrays-in-schemas) are also supported as described later.
121
122#### Validation
123
124For the most part, we favor sanitization over validation. It's better to figure out what the user meant on the server side than to give them a hard time. But sometimes validation is unavoidable.
125
126You can make any field mandatory by giving it the `required: true` attribute. Currently this is only implemented in browser-side JavaScript, so your server-side code should be prepared not to crash if a property is unexpectedly empty.
127
128If the user attempts to save without completing a required field, the `apos-error` class will be set on the `fieldset` element for that field, and `schemas.convertFields` will pass an error to its callback. If `schemas.convertFields` passes an error, your code should not attempt to save the object or close the dialog in question, but rather let the user continue to edit until the callback is invoked with no error.
129
130If `schemas.convertFields` does pass an error, you may invoke:
131
132`aposSchemas.scrollToError($el)`
133
134To ensure that the first error in the form is visible.
135
136If you are performing your own custom validation, you can call:
137
138`aposSchemas.addError($el, 'body')`
139
140To indicate that the field named `body` has an error, in the same style that is applied to errors detected via the schema.
141
142#### Grouping fields into tabs
143
144One lonnnnng scrolling list of fields is usually not user-friendly.
145
146You may group fields together into tabs instead using the `groupFields` option. Here's how you would do it if you wanted to override our tab choices for the blog:
147
148```javascript
149modules: {
150 'apostrophe-blog': {
151 groupFields: [
152 // We don't list the title field so it stays on top
153 {
154 name: 'content',
155 label: 'Content',
156 icon: 'content',
157 fields: [
158 'thumbnail', 'body'
159 ]
160 },
161 {
162 name: 'details',
163 label: 'Details',
164 icon: 'metadata',
165 fields: [
166 'slug', 'published', 'publicationDate', 'publicationTime', 'tags'
167 ]
168 }
169 ]
170 }
171}
172```
173
174Each group has a name, a label, an icon (passed as a CSS class on the tab's icon element), and an array of field names.
175
176In `app.js`, you can simply pass `groupFields` like any other option when configuring a module. *The last call to `groupFields` wins, overriding any previous attempts to group the fields, so be sure to list all of them* except for fields you want to stay visible at all times above the tabs.
177
178**Be aware that group names must be distinct from field names.** Apostrophe will stop the music and tell you if they are not.
179
180#### Arrays in schemas
181
182Let's say you're managing "companies," and each company has several "offices." Wouldn't it be nice if you could edit a list of offices while editing the company?
183
184This is easy to do with the `array` field type:
185
186```javascript
187{
188 name: 'offices',
189 label: 'Offices',
190 type: 'array',
191 minimize: true,
192 schema: [
193 {
194 name: 'city',
195 label: 'City',
196 type: 'string'
197 },
198 {
199 name: 'zip',
200 label: 'Zip',
201 type: 'string',
202 def: '19147'
203 },
204 {
205 name: 'thumbnail',
206 label: 'Thumbnail',
207 type: 'singleton',
208 widgetType: 'slideshow',
209 options: {
210 limit: 1
211 }
212 }
213 ]
214}
215```
216
217Each `array` has its own `schema`, which supports all of the usual field types. You an even nest an `array` in another `array`.
218
219The `minimize` option of the array field type enables editors to expand or hide fields of an array in order to more easily work with longer lists of items or items with very long schemas.
220
221It's easy to access the resulting information in a page template, such as the `show` template for companies. The array property is... you guessed it... an array! Not hard to iterate over at all:
222
223```markup
224<h4>Offices</h4>
225<ul>
226 {% for office in item.offices %}
227 <li>{{ office.city }}, {{ office.zip }}</li>
228 {% endfor %}
229</ul>
230```
231
232Areas and thumbnails are allowed in arrays. In order to display them in a page template, you'll need to use this syntax:
233
234```markup
235{% for office in item.offices %}
236 {{ aposSingleton({ area: office.thumbnail, type: 'slideshow', more options... }) }}
237{% endfor %}
238```
239
240For an area you would write:
241
242```markup
243{% for office in item.offices %}
244 {{ aposArea({ area: office.body, more options... }) }}
245{% endfor %}
246```
247
248Since the area is not a direct property of the page, we can't use the `(page, areaname)` syntax that is typically more convenient.
249
250Areas and thumbnails in arrays **can** be edited "in context" on a page.
251
252#### Preventing Autocomplete
253
254For most field types, you may specify:
255
256 autocomplete: false
257
258To request that the browser not try to autocomplete the field's value for the user. The only fields that do not support this are those that are not implemented by a traditional HTML form field, and in all probability browsers won't autocomplete these anyway.
259
260### Editing: Schemas in Nunjucks Templates
261
262This is really easy! Just write this in your nunjucks template:
263
264```jinja
265{% include 'schemas:schemaMacros.html' %}
266
267<form class="my-form">
268 {{ schemaFields(schema) }}
269</form>
270```
271
272Of course you must pass your schema to Nunjucks when rendering your template.
273
274All of the fields will be presented with their standard markup, ready to be populated by `aposSchemas.populateFields` in browser-side JavaScript.
275
276You may want to customize the way a particular field is output. The most future-proof way to do this is to use the `custom` option and pass your own macro:
277
278```markup
279{% macro renderTitle(field) %}
280<fieldset data-name="{{ field.name }}" class="super-awesome">
281 special awesome title: <input name="{{ field.name }}" />
282</fieldset>
283{% endmacro %}
284
285<form>
286 {{
287 schemaFields(schema, {
288 custom: {
289 title: renderTitle
290 }
291 })
292 }}
293</form>
294```
295
296This way, Apostrophe outputs all of the fields for you, grouped into the proper tabs if any, but you still get to use your own macro to render this particular field.
297
298If you want to include the standard rendering of a field as part of your custom output, use the `aposSchemaField` helper function:
299
300`aposSchemaField(field)`
301
302This will decorate the field with a `fieldset` in the usual way.
303
304Note that the user's current values for the fields, if any, are added by browser-side JavaScript. You are not responsible for that in your template.
305
306You also need to push your schema from the server so that it is visible to browser-side Javascript:
307
308```javascript
309self._apos.pushGlobalData({
310 mymodule: {
311 schema: self.schema
312 }
313});
314```
315
316### Editing: Browser-Side Javascript
317
318Now you're ready to use the browser-side JavaScript to power up the editor. Note that the `populateFields` method takes a callback:
319
320```javascript
321var schema = apos.data.mymodule.schema;
322aposSchemas.populateFields($el, schema, object, function() {
323 // We're ready
324});
325```
326
327`$el` should be a jQuery object referring to the element that contains all of the fields you output with `schemaFields`. `object` is an existing object containing existing values for some or all of the properties.
328
329And, when you're ready to save the content:
330
331```javascript
332aposSchemas.convertFields($el, schema, object)
333```
334
335This is the same in reverse. The properties of the object are set based on the values in the editor. Aggressive sanitization is not performed in the browser because the server must always do it anyway (never trust a browser). You may of course do your own validation after calling `convertFields` and perhaps decide the user is not done editing yet after all.
336
337### Editing: Saving Objects On the Server
338
339Serializing the object and sending it to the server is up to you. (We recommend using `$.jsonCall`.) But once it gets there, you can use the `convertFields` method to clean up the data and make sure it obeys the schema. The incoming fields should be properties of `data`, and will be sanitized and copied to properties of `object`. Then the callback is invoked:
340
341```javascript
342schemas.convertFields(req, schema, 'form', data, object, callback)
343```
344
345The third argument is set to `'form'` to indicate that this data came from a form and should go through that converter.
346
347Now you can save `object` as you normally would.
348
349### Editing Fields In Context
350
351For snippets, the entire object is usually edited in a modal dialog. But if you are using schemas to enhance regular pages via the [apostrophe-fancy-pages](https://github.com/punkave/apostrophe-fancy-pages) module, you might prefer to edit certain areas and singletons "in context" on the page itself.
352
353You could just leave them out of the schema, and take advantage of Apostrophe's support for "spontaneous areas" created by `aposArea` and `aposSingleton` calls in templates.
354
355An alternative is to set `contextual` to `true` for such fields. This will keep them out of forms generated by `{{ schemaFields }}`, but will not prevent you from taking advantage of other features of schemas, such as CSV import.
356
357Either way, it is your responsibility to add an appropriate `aposArea` or `aposSingleton` call to the page, as you are most likely doing already.
358
359### Joins in Schemas
360
361You may use the `join` type to automatically pull in related objects from this or another module. Typical examples include fetching events at a map location, or people in a group. This is very cool.
362
363*"Aren't joins bad? I read that joins were bad in some NoSQL article."*
364
365Short answer: no.
366
367Long answer: sometimes. Mostly in so-called "webscale" projects, which have nothing to do with 99% of websites. If you are building the next Facebook you probably know that, and you'll denormalize your data instead and deal with all the fascinating bugs that come with maintaining two copies of everything.
368
369Of course you have to be smart about how you use joins, and we've included options that help with that.
370
371##### One-To-One Joins
372
373You might write this:
374
375```javascript
376 addFields: [
377 {
378 name: '_location',
379 type: 'joinByOne',
380 withType: 'mapLocation',
381 idField: 'locationId',
382 label: 'Location'
383 }
384 ]
385}
386```
387
388(How does this work? `apostrophe-schemas` will consult the `apostrophe-pages` module to find the manager object responsible for `mapLocation` objects, which will turn out to be the `apostrophe-map` module.)
389
390Now the user can pick a map location. And if you call `schema.join(req, schema, myObjectOrArrayOfObjects, callback)`, `apostrophe-schemas` will carry out the join, fetch the related object and populate the `_location` property of your object. Note that it is much more efficient to pass an array of objects if you need related objects for more than one.
391
392Here's an example of using the resulting ._location property in a Nunjucks template:
393
394```twig
395{% if item._location %}
396 <a href="{{ item._location.url | e }}">Location: {{ item._location.title | e }}</a>
397{% endif %}
398```
399
400The id of the map location actually "lives" in the `location_id` property of each object, but you won't have to deal with that directly.
401
402*Always give your joins a name starting with an underscore.* This warns Apostrophe not to store this information in the database permanently where it will just take up space, then get re-joined every time anyway.
403
404Currently after the user has selected one item they see a message reading "Limit Reached!" We realize this may not be the best way of indicating that a selection has already been made. So you may pass a `limitText` option with an alternative message to be displayed at this point.
405
406##### Joining With "Regular Pages"
407
408What if you want to allow the user to pick anything at all, as long as it's a "regular page" in the page tree with its own permanent URL?
409
410Just use:
411
412```javascript
413withType: 'page'
414```
415
416This special case allows you to easily build navigation menus and the like using [schema widgets](https://github.com/punkave/apostrophe-schema-widgets) and [array fields](#arrays-in-schemas).
417
418##### Reverse Joins
419
420You can also join back in the other direction:
421
422```javascript
423 addFields: [
424 {
425 name: '_events',
426 type: 'joinByOneReverse',
427 withType: 'event',
428 idField: 'locationId',
429 label: 'Events'
430 }
431 ]
432```
433
434Now, in the `show` template for the map module, we can write:
435
436```twig
437{% for event in item._events %}
438 <h4><a href="{{ event.url | e }}">{{ event.title | e }}</a></h4>
439{% endfor %}
440```
441
442"Holy crap!" Yeah, it's pretty cool.
443
444Note that the user always edits the relationship on the "owning" side, not the "reverse" side. The event has a `location_id` property pointing to the map, so users pick a map location when editing an event, not the other way around.
445
446##### Nested Joins: You Gotta Be Explicit
447
448*"Won't this cause an infinite loop?"* When an event fetches a location and the location then fetches the event, you might expect an infinite loop to occur. However Apostrophe does not carry out any further joins on the fetched objects unless explicitly asked to.
449
450*"What if my events are joined with promoters and I need to see their names on the location page?"* If you really want to join two levels deep, you can "opt in" to those joins:
451
452```javascript
453 addFields: [
454 {
455 name: '_events',
456 // Details of the join, then...
457 withJoins: [ '_promoters' ]
458 }
459 ]
460```
461
462This assumes that `_promoters` is a join you have already defined for events.
463
464*"What if my joins are nested deeper than that and I need to reach down several levels?"*
465
466You can use "dot notation," just like in MongoDB:
467
468```javascript
469withJoins: [ '_promoters._assistants' ]
470```
471
472This will allow events to be joined with their promoters, and promoters to be joined with their assistants, and there the chain will stop.
473
474You can specify more than one join to allow, and they may share a prefix:
475
476```javascript
477withJoins: [ '_promoters._assistants', '_promoters._bouncers' ]
478```
479
480Remember, each of these later joins must actually be present in the configuration for the module in question. That is, "promoters" must have a join called "_assistants" defined in its schema.
481
482##### Nested Joins and Arrays
483
484Joins are allowed in the schema of an [array field](#arrays-in-schemas), and they work exactly as you would expect. Just include joins in the schema for the array as you normally would.
485
486And if you are carrying out a nested join with the `withJoins` option, you'll just need to refer to the join correctly.
487
488Let's say that each promoter has an array of ads, and each ad is joined to a media outlet. We're joing with events, which are joined to promoters, and we want to make sure media outlets are included in the results.
489
490So we write:
491
492```javascript
493 addFields: [
494 {
495 name: '_events',
496 // Details of the join, then...
497 withJoins: [ '_promoters.ads._mediaOutlet' ]
498 }
499 ]
500```
501
502#### Many-To-Many Joins
503
504Events can only be in one location, but stories can be in more than one book, and books also contain more than one story. How do we handle that?
505
506Consider this configuration for a `books` module:
507
508```javascript
509 addFields: [
510 {
511 name: '_stories',
512 type: 'joinByArray',
513 withType: 'story',
514 idsField: 'storyIds',
515 sortable: true,
516 label: 'Stories'
517 }
518 ]
519```
520
521Now we can access all the stories from the show template for books (or the index template, or pretty much anywhere):
522
523```twig
524<h3>Stories</h3>
525{% for story in item._stories %}
526 <h4><a href="{{ story.url | e }}">{{ story.title | e }}</a></h4>
527{% endfor %}
528```
529
530*Since we specified `sortable:true`*, the user can also drag the list of stories into a preferred order. The stories will always appear in that order in the `._stories` property when examinining a book object.
531
532*"Many-to-many... sounds like a LOT of objects. Won't it be slow and use a lot of memory?"*
533
534It's not as bad as you think. Apostrophe typically fetches only one page's worth of items at a time in the index view, with pagination links to view more. Add the objects those are joined to and it's still not bad, given the performance of v8.
535
536But sometimes there really are too many related objects and performance suffers. So you may want to restrict the join to occur only if you have retrieved only *one* book, as on a "show" page for that book. Use the `ifOnlyOne` option:
537
538```javascript
539'stories': {
540 addFields: [
541 {
542 name: '_books',
543 withType: 'book',
544 ifOnlyOne: true,
545 label: 'Books'
546 }
547 ]
548}
549```
550
551Now any call to `schema.join` with only one object, or an array of only one object, will carry out the join with stories. Any call with more than one object won't.
552
553Hint: in index views of many objects, consider using AJAX to load related objects when the user indicates interest rather than displaying related objects all the time.
554
555Another way to speed up joins is to limit the fields that are fetched in the join. You may pass options such as `fields` to the `get` method used to actually fetch the joined objects. Note that `apos.get` and everything derived from it, like `snippets.get`, will accept a `fields` option which is passed to MongoDB as the projection:
556
557```javascript
558'stories': {
559 addFields: [
560 {
561 name: '_books',
562 withType: 'book',
563 label: 'Books',
564 getOptions: {
565 fields: { title: 1, slug: 1 }
566 }
567 }
568 ]
569}
570```
571
572If you are just linking to things, `{ title: 1, slug: 1 }` is a good projection to use. You can also include specific areas by name in this way.
573
574#### Reverse Many-To-Many Joins
575
576We can also access the books from the story if we set the join up in the stories module as well:
577
578```javascript
579 addFields: [
580 {
581 name: '_books',
582 type: 'joinByArrayReverse',
583 withType: 'book',
584 idsField: 'storyIds',
585 label: 'Books'
586 }
587 ]
588}
589```
590
591Now we can access the `._books` property for any story. But users still must select stories when editing books, not the other way around.
592
593#### When Relationships Get Complicated
594
595What if each story comes with an author's note that is specific to each book? That's not a property of the book, or the story. It's a property of *the relationship between the book and the story*.
596
597If the author's note for every each appearance of each story has to be super-fancy, with rich text and images, then you should make a new module that subclasses snippets in its own right and just join both books and stories to that new module. You can also use [array fields](#arrays-in-schemas) in creative ways to address this problem, using `joinByOne` as one of the fields of the schema in the array.
598
599But if the relationship just has a few simple attributes, there is an easier way:
600
601```javascript
602 addFields: [
603 {
604 name: '_stories',
605 label: 'Stories',
606 type: 'joinByArray',
607 withType: 'story',
608 idsField: 'storyIds',
609 relationshipsField: 'storyRelationships',
610 relationship: [
611 {
612 name: 'authorsNote',
613 type: 'string'
614 }
615 ],
616 sortable: true
617 }
618 ]
619```
620
621Currently "relationship" properties can only be of type `string` (for text), `select` or `boolean` (for checkboxes). Otherwise they behave like regular schema properties.
622
623*Warning: the relationship field names `label` and `value` must not be used.* These names are reserved for internal implementation details.
624
625Form elements to edit relationship fields appear next to each entry in the list when adding stories to a book. So immediately after adding a story, you can edit its author's note.
626
627Once we introduce the `relationship` option, our templates have to change a little bit. The `show` page for a book now looks like:
628
629```twig
630{% for story in item._stories %}
631 <h4>Story: {{ story.item.title | e }}</h4>
632 <h5>Author's Note: {{ story.relationship.authorsNote | e }}</h5>
633{% endfor %}
634```
635
636Two important changes here: *the actual story is `story.item`*, not just `story`, and *relationship fields can be accessed via `story.relationship`*. This change kicks in when you use the `relationship` option.
637
638Doing it this way saves a lot of memory because we can still share book objects between stories and vice versa.
639
640#### Accessing Relationship Properties in a Reverse Join
641
642You can do this in a reverse join too:
643
644```javascript
645 addFields: [
646 {
647 name: '_books',
648 type: 'joinByArrayReverse',
649 withType: 'book',
650 idsField: 'storyIds',
651 relationshipsField: 'storyRelationships',
652 relationship: [
653 {
654 name: 'authorsNote',
655 type: 'string'
656 }
657 ]
658 }
659 ]
660```
661
662Now you can write:
663
664```twig
665{% for book in item._books %}
666 <h4>Book: {{ book.item.title | e }}</h4>
667 <h5>Author's Note: {{ book.relationship.authorsNote | e }}</h5>
668{% endfor %}
669```
670
671As always, the relationship fields are edited only on the "owning" side (that is, when editing a book).
672
673*"What is the `relationshipsField` option for? I don't see `story_relationships` in the templates anywhere."*
674
675Apostrophe stores the actual data for the relationship fields in `story_relationships`. But since it's not intuitive to write this in a template:
676
677```twig
678{# THIS IS THE HARD WAY #}
679{% for story in book._stories %}
680 {{ story.item.title | e }}
681 {{ book.story_relationships[story._id].authorsNote | e }}
682{% endif %}
683```
684
685Apostrophe instead lets us write this:
686
687```twig
688{# THIS IS THE EASY WAY #}
689{% for story in book._stories %}
690 {{ story.item.title | e }}
691 {{ story.relationship.authorsNote | e }}
692{% endif %}
693```
694
695*Much better.*
696
697#### Specifying Joins When Calling schemas.join
698
699Sometimes you won't want to honor all of the joins that exist in your schema. Other times you may wish to fetch more than your schema's `withJoin` options specify as a default behavior.
700
701You can force `schemas.join` to honor specific joins by supplying a `withJoins` parameter:
702
703```javascript
704schemas.join(req, schema, objects, [ '_locations._events._promoters' ], callback);
705```
706
707The syntax is exactly the same as for the `withJoins` option to individual joins in the schema, discussed earlier.
708
709### Overriding Templates For Individual Fields
710
711You can override templates for individual fields without resorting to writing your own `new.html` and `edit.html` templates from scratch.
712
713Here's the `string.html` template that renders all fields with the `string` type by default:
714
715```html
716{% include "schemaMacros.html" %}
717{% if textarea %}
718 {{ formTextarea(name, label) }}
719{% else %}
720 {{ formText(name, label) }}
721{% endif %}
722```
723
724You can override these for your project by creating new templates with the same names in the `lib/modules/apostrophe-schemas/views` folder. This lets you change the appearance for every field of a particular type. You should only override what you really wish to change.
725
726In addition, you can specify an alternate template name for an individual field in your schema:
727
728{
729 type: 'integer',
730 name: 'shoeSize',
731 label: 'Shoe Size',
732 template: 'shoeSize'
733}
734
735This will cause the `shoeSize.html` template to be rendered instead of the `integer.html` template.
736
737You can also pass a `render` function, which receives the field object as its only parameter. Usually you'll find it much more convenient to just use a string and put your templates in `lib/modules/apostrophe-schemas/views`.
738
739### Adding New Field Types
740
741You can add a new field type easily.
742
743On the server side, we'll need to write three methods:
744
745* A "render" method that just renders a suitable Nunjucks template to insert this type of field in a form. Browser-side JavaScript will populate it with content later. Use the assets mixin in your module to make this code easy to write.
746* A converter for use when a form submission arrives.
747* A converter for use during CSV import of an object.
748
749The converter's job is to ensure the content is really a list of strings and then populate the object with it. We pull the list from `data` (what the user submitted) and use it to populate `object`. We also have access to the field name (`name`) and, if we need it, the entire field object (`field`), which allows us to implement custom options.
750
751**Your converter must not set the property to undefined or delete the property.** It must be possible to distinguish a property that has been set to a value, even if that value is `false` or `null` or `[]`, from one that is currently undefined and should therefore display the default.
752
753Here's an example of a custom field type: a simple list of strings.
754
755```javascript
756
757// Earlier in our module's constructor...
758self._apos.mixinModuleAssets(self, 'mymodulename', __dirname, options);
759// Now self.renderer is available
760
761schemas.addFieldType({
762 name: 'list',
763 render: self.renderer('schemaList'),
764 converters: {
765 form: function(req, data, name, object, field, callback) {
766 // Don't trust anything we get from the browser! Let's sanitize!
767
768 var maybe = _.isArray(data[name]) ? data[name] || [];
769
770 // Now build up a list of clean content
771 var yes = [];
772
773 _.each(maybe, function(item) {
774 if (field.max && (yes.length >= field.max)) {
775 // Limit the length of the list via a "max" property of the field
776 return;
777 }
778 // Only accept strings
779 if (typeof(item) === 'string') {
780 yes.push(item);
781 }
782 });
783 object[name] = yes;
784 return setImmediate(function() {
785 return callback(null);
786 });
787 },
788
789 // CSV is a lot simpler because the input is always just
790 // a string. Split on "|" to allow more than one string in the list
791 csv: function(req, data, name, object, field, callback) {
792 object[name] = data[name].split('|');
793 return setImmediate(function() {
794 return callback(null);
795 });
796 }
797 }
798});
799```
800
801We can also supply an optional `indexer` method to allow site-wide searches to locate this object based on the value of the field:
802
803```javascript
804 indexer: function(value, field, texts) {
805 var silent = (field.silent === undefined) ? true : field.silent;
806 texts.push({ weight: field.weight || 15, text: value.join(' '), silent: silent });
807 }
808```
809
810And, if our field modifies properties other than the one matching its `name`, we must supply a `copier` function so that the `subsetInstance` method can be used to edit personal profiles and the like:
811
812```javascript
813 copier: function(name, from, to, field) {
814 // Note: if this is really all you need, you can skip
815 // writing a copier
816 to[name] = from[name];
817 }
818```
819
820The `views/schemaList.html` template should look like this. Note that the "name" and "label" options are passed to the template. In fact, all properties of the field that are part of the schema are available to the template. Setting `data-name` correctly is crucial. Adding a CSS class based on the field name is a nice touch but not required.
821
822```jinja
823<fieldset class="apos-fieldset my-fieldset-list apos-fieldset-{{ name | css}}" data-name="{{ name }}">
824 <label>{{ label | e }}</label>
825 {# Text entry for autocompleting the next item #}
826 <input name="{{ name | e }}" data-autocomplete placeholder="Type Here" class="autocomplete" />
827 {# This markup is designed for jQuery Selective to show existing list items #}
828 <ul data-list class="my-list">
829 <li data-item>
830 <span class="label-and-remove">
831 <a href="#" class="apos-tag-remove icon-remove" data-remove></a>
832 <span data-label>Example label</span>
833 </span>
834 </li>
835 </ul>
836</fieldset>
837```
838
839Next, on the browser side, we need to supply two methods: a displayer and a converter.
840
841"displayer" is a method that populates the form field. `aposSchemas.populateFields` will invoke it.
842
843"converter" is a method that retrieves data from the form field and places it in an object. `aposSchemas.convertFields` will invoke it.
844
845Here's the browser-side code to add our "list" type:
846
847```javascript
848aposSchemas.addFieldType({
849 name: 'list',
850 displayer: function(data, name, $field, $el, field, callback) {
851 // $field is the element with right "name" attribute, which is great
852 // for classic HTML form elements. But for this type we want the
853 // div with the right "data-name" attribute. So find it in $el
854 $field = $el.find('[data-name="' + name + '"]');
855 // Use jQuery selective to power the list
856 $field.selective({
857 // pass the existing values in as label/value pairs to satisfy
858 // jQuery selective
859 data: [
860 _.map(data[name], function() {
861 return {
862 label: data[name],
863 value: data[name]
864 };
865 });
866 ],
867 // Allow the user to add new strings
868 add: true
869 });
870 // Be sure to invoke the callback
871 return callback();
872 },
873 converter: function(data, name, $field, $el, field, callback) {
874 $field = $el.find('[data-name="' + name + '"]');
875 data[name] = $field.selective('get');
876 // Be sure to invoke the callback
877 return callback();
878 }
879});
880```
881
882This code can live in `site.js`, or in a `js` file that you push as an asset from your project or an npm module. Make sure your module loads *after* `apostrophe-schema`.
883
884### Creating Schemas With Compose and Refine
885
886For many applications just creating your own array of fields is fine. But if you are creating a subclass of another module that also uses schemas, and you want to adjust the schema, you'll be a lot happier if the superclass uses the `schemas.compose()` method to build up the schema via the `addFields`, `removeFields`, `orderFields` and occasionally `alterFields` options.
887
888Here's a simple example:
889
890```javascript
891schemas.compose({
892 addFields: [
893 {
894 name: 'title',
895 type: 'string',
896 label: 'Name'
897 },
898 {
899 name: 'age',
900 type: 'integer',
901 label: 'Age'
902 }
903 },
904 removeFields: [ 'age' ]
905 ]
906});
907```
908
909This `compose` call adds two fields, then removes one of them. This makes it easy for subclasses to contribute to the object which a parent class will ultimately pass to `compose`. It often looks like this:
910
911```javascript
912var schemas = require('apostrophe-schemas');
913
914// Superclass has title and age fields, also merges in any fields appended
915// to addFields by a subclass
916
917function MySuperclass(options) {
918 var self = this;
919 options.addFields = [
920 {
921 name: 'title',
922 type: 'string',
923 label: 'Name'
924 },
925 {
926 name: 'age',
927 type: 'integer',
928 label: 'Age'
929 }
930 ].concat(options.addFields || []);
931 self._schema = schemas.compose(options);
932}
933
934// Subclass removes the age field, adds the shoe size field
935
936function MySubclass(options) {
937 var self = this;
938 MySuperclass.call(self, {
939 addFields: [
940 {
941 name: 'shoeSize',
942 title: 'Shoe Size',
943 type: 'string'
944 }
945 ],
946 removeFields: [ 'age' ]
947 });
948}
949```
950
951#### Removing Fields
952
953You can also specify a `removeFields` option which will remove some of the fields you passed to `addFields`.
954
955This is useful if various subclasses are contributing to your schema.
956
957```javascript
958removeFields: [ 'thumbnail', 'body' ]
959}
960```
961
962#### Changing the Order of Fields
963
964When adding fields, you can specify where you want them to appear relative to existing fields via the `before`, `after`, `start` and `end` options. This works great with the subclassing technique shown above:
965
966```javascript
967addFields: [
968 {
969 name: 'favoriteCookie',
970 type: 'string',
971 label: 'Favorite Cookie',
972 after: 'title'
973 }
974]
975```
976
977Any additional fields after `favoriteCookie` will be inserted with it, following the title field.
978
979Use the `before` option instead of `after` to cause a field to appear before another field.
980
981Use `start: true` to cause a field to appear at the top.
982
983Use `start: end` to cause a field to appear at the end.
984
985If this is not enough, you can explicitly change the order of the fields with `orderFields`:
986
987```javascript
988orderFields: [ 'year', 'specialness' ]
989```
990
991Any 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).
992
993#### Requiring Many Fields
994
995Although `required: true` works well, if you are subclassing and you wish to require a number of previously optional fields, the `requiredFields` option is more convenient. This is especially handy when working with `apostrophe-moderator`:
996
997```javascript
998requireFields: [ 'title', 'startDate', 'body' ]
999```
1000
1001#### Altering Fields: The Easy Way
1002
1003You can specify the same field twice in your `addFields` array. The last occurrence wins.
1004
1005#### Altering Fields: The Hard Way
1006
1007There is also an `alterFields` option available. This must be a function which receives the schema (an array of fields) as its argument and modifies it. Most of the time you will not need this option; see `removeFields`, `addFields`, `orderFields` and `requireFields`. 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."
1008
1009#### Refining Existing Schemas With `refine`
1010
1011Sometimes you'll want a modified version of an existing schema. `schemas.refine` is the simplest way to do this:
1012
1013var newSchema = schemas.refine(schema, {
1014 addFields: ...,
1015 removeFields ..., etc
1016});
1017
1018The options are exactly the same as the options to `compose`. The returned array is a copy. No modifications are made to the original schema array.
1019
1020#### Filtering existing schemas with `subset`
1021
1022If you just want to keep certain fields in your schema, while maintaining the same tab groups, use the `subset` method. This method will discard any unwanted fields, as well as any groups that are empty in the new subset of the schema:
1023
1024```javascript
1025// A subset suitable for people editing their own profiles
1026var profileSchema = schemas.subset(schema, [ 'title', 'body', 'thumbnail' ]);
1027```
1028
1029If you wish to apply new groups to the subset, use `refine` and `groupFields`.
1030
1031#### Creating new objects with `newInstance`
1032
1033The `newInstance` method can be used to create an object which has the appropriate default value for every schema field:
1034
1035```javascript
1036var snowman = schemas.newInstance(snowmanSchema);
1037```
1038
1039#### Filtering object properties with `subsetInstance`
1040
1041The `subsetInstance` method accepts a schema and an existing instance object and returns a new object with only the properties found in the given schema. This includes not just the obvious properties matching the `name` of each field, but also any `idField` or `idsField` properties specified by joins.
1042
1043```javascript
1044var profileSchema = schemas.subset(people.schema, [ 'title', 'body', 'thumbnail' ]);
1045var profile = schemas.subsetInstance(person, profileSchema);
1046```