Understanding of how core components work is essential for building applications with Trikoder Trim. This chapter provides insight into resource controller, resource list, resource edit, router, navigation, service container and application object.
Resource controller component is a central place to define user interface for given resource. Here we define how resource is browsed, filtered and sorted in list, what form fields are rendered when resource is created or updated.
In most use cases controller looks like a simple configuration file. This configuration based architecture delegates most of the work to components that are composed out of our sight. Resource controller is a container component which calls service components for listing and editing resources who build their own component subtrees - all that is abstracted from user via simple and easy to use API.
All resource controllers share same basic skeleton. We define what resource type controller is handling (resourceName) and implement methods for resource listing (setupList) and resource editing (setupEdit):
export default {
resourceName: 'tag',
setupList: function({list}) {
// how is resource listed?
},
setupEdit: function({edit}) {
// how is resource edited?
}
};
A list of controller properties and methods is examined bellow:
Controller property where we name the resource handled (tag, article, page...)
resourceName: 'tag'
Controller property where we define resource caption mapping, default value is null. It is currently used only in mass actions component as a value for 'mapSelectedCaptionsTo' (if it is null, then it fallbacks to 'id') so it's recommended to always set resource caption value.
resourceCaption: 'title'
When 2 or more different UI controllers that connect to same resource (resourceName) are needed this property is used to separate them logically. Resource alias will be used to construct different controller routes and to bind correct selected main navigation item.
resourceCaption: 'userTag'
Property where we define if draft resource is needed when resource is created. When createRequiresDraft is set to true controller will save empty resource object before creating interface is displayed. Saved draft resource will receive id and be able to support related objects. False is default value.
createRequiresDraft: false
Property which defines which strategy is used for persisting main and related resources:
- relatedFirst: related resources are saved before main resource (default behavior)
- relatedLast: related resources are saved after main resource is persisted
createRelatedStrategy: 'relatedFirst'
Specify relationships to include in API payload for resource listing (index) and editing (edit) Previously named "includeApiData" in versions up to v0.77, now deprecated.
includedRelationships: {
index: ['category'],
edit: ['category', 'author']
}
Specify fields to include in API payload for resource listing (index) and editing (edit)
includedFields: {
index: {
article: ['title', 'intro', 'author', 'tags'],
user: ['email']
}
}
Controller method where we define how resource is browsed, filtered and sorted in list view.
setupList: function({list, query}) {
this.addCreateControl('Create new tag');
list.addFilter('TextFormElement', {
name: 'title',
label: 'Title'
});
list.addItem('TextListItem', {
caption: 'ID',
mapTo: 'id'
});
list.addItem('LinkListItem', {
caption: 'Title',
mapTo: 'title',
action: 'editItem'
});
},
Following arguments are available:
- list: instance of resourceList object
- query: current resource query parameters (filters, sort, page...)
Controller method where we define form fields rendered when resource is created or updated.
setupEdit: function({edit, method, resourceModel}) {
this.addToIndexControl().addSaveControl();
edit.addField('TextFormElement', {
label: 'Title',
name: 'title'
});
}
Following arguments are available:
- edit: instance of editHandler object
- method: current resource authoring context ('edit' or 'create')
- resourceModel: instance of resource model that is currently edited (or created)
Controller instance method which opens resource index or listing.
controller.openIndex(queryParams);
Controller instance method which opens resource editing.
controller.openEdit(routeParams, queryParams);
Controller instance method which opens resource create.
controller.openCreate(queryParams);
Controller instance method which adds resource create control.
controller.addCreateControl(caption);
Controller instance method which adds resource save control.
controller.addSaveControl(caption);
Controller instance method which adds resource list control.
controller.addToIndexControl();
Controller instance method which adds generic resource control.
controller.addControl({
caption: 'myControl',
url: this.getCreateUrl(),
className: 'accented iconPlus',
action: this.openCreate
});
Controller instance method which adds generic dropdown resource control.
controller.addDropdownControl(params);
Resource list is component responsible for handling resource browsing, filtering and sorting. Examine list elements chapter to find out how each list element is configured.
Method for defining what elements are displayed when resource is listed:
list.addItem('TextListItem', {
caption: 'Title',
mapTo: 'title'
});
Method for adding filter form elements when resource is listed:
list.addFilter('TextFormElement', {
name: 'title',
label: 'Title'
});
Method for adding sort options on resource list:
list.addSort([
{
label: 'By title',
field: '-title'
},
{
label: 'By date',
field: '-date'
}
]);
Method for setting persistent API filters.
list.filterAlwaysBy('deleted', 'no');
// or
list.filterAlwaysBy({deleted: 'no'});
Method for setting initial API and UI filters.
list.filterInitiallyBy('published', 'yes');
// or
list.filterInitiallyBy({published: 'yes'});
Call to refresh current list context content
list.refreshItems();
Method for choosing which template is used for resource listing ('table' and 'cards' are currently supported).
list.setTemplate('cards');
Method for adding mass actions to resource list.
list.addMassAction([
{
caption: 'Publish',
updateAttributes: {published: true}
}
]);
Resource edit is component responsible for handling how resource is created or updated.
Method for defining what form elements are mapped to resource attributes and relations when resource is edited or created. Examine form elements chapter to find out how each form element is configured.
edit.addField('TextFormElement', {
label: 'Title',
name: 'title'
});
Show edit form field:
edit.showField('title');
Show edit form field:
edit.hideField('title');
Toggle edit form field (use second boolean argument to show or hide field)
edit.toggleField('title', true);
Update edit field value and options / props
edit.updateField('title', {
value: 'Foo'
});
Update all edit fields options
edit.updateAllFields({
editable: false
});
Observe and respond to form field value changes:
edit.observe('formData.title', value => {
console.log('Title changed: ' + value);
})
Allow or disallow resource saving:
edit.allowSave(false);
Method for configuring edit and create layout. Supports tabs, regions and groups (all items are auto generated from layout reference).
// assign form element to layout region (main and side regions are supported) position with layoutReference
edit.addField(TextFormElement, {
label: 'Title',
name: 'title',
layoutReference: 'mainRegion'
});
edit.addField(TextFormElement, {
label: 'Meta data',
name: 'meta',
layoutReference: 'sideRegion'
});
Altering layout configuration to set tab captions:
// configure tab layout caption
edit.configureLayout({
'mainTab.caption': 'Content and settings',
'seoTab.caption': 'SEO and meta data'
});
// assign form element to layout position with layoutReference
editHandler.addField(TextFormElement, {
label: 'Title',
name: 'title',
layoutReference: 'mainTab.mainRegion'
});
Select edit layout tab:
edit.selectTab('main');
Application utilizes simple service container to register and locate components and services. All form and list components are registered and retrieved from service container by default.
Your resource controllers should also be registered as services. We encourage you to do so with dynamic import to utilize your bundler’s code splitting and load controller code only when it is requested.
A typical service container with navigation and few registered controllers looks something like this:
export default {
MainNavigation: () => import('./controllers/mainNavigation.js'),
PageController: () => import('./controllers/page.js'),
TagController: () => import('./controllers/tag.js')
};
If you need to access service container manually somewhere in your code:
import serviceContainer from 'trim/library/serviceContainer.js';
serviceContainer.register('myService', () => {foo: 'bar'});
serviceContainer.get('myService').then(myService => {
console.log(myService.foo); // outputs bar
});
Application utilizes extended VueJS router for linking and routing needs. Your application routes typically look something like this:
export default router => {
router.controller('/', 'dashboard', 'Page@index');
router.resource('page');
router.resource('tag');
router.controller('my-settings', 'mySettings', 'MySettings');
};
Method for defining routes with controller components.
router.controller(path, routeName, controllerString);
// ... is somewhat equivalent to...
this.addRoute({
path: '/' + path,
name: routeName,
component: AdminDefault,
props: => {
// set controller from service registry as child component of Default Admin component
// if method is defined it will be called after component is created
}
});
Router resource method is a shortcut for defining 3 most used routes for resource:
router.resource('tag');
// ... is equivalent to...
router.controller('tag/create', 'resource.tag.create', 'Tag@create');
router.controller('tag/:id', 'resource.tag.edit', 'Tag@edit');
router.controller('tag', 'resource.tag.index', 'Tag@index');
Generate links with router via route names and params:
router.url('resource.tag.index') // /tag;
router.url('resource.tag.edit', {id: 2}) // /tag/2;
router.url('resource.tag.edit', {id: 2}, {foo: 3}) // /tag/2?foo=3;
Navigation component is used to define main user navigation UI element. Navigation links, user panel links, application name and current username are all defined in navigation component.
export default {
getNavigationItems: router => [
{
caption: 'Pages',
key: 'page',
url: router.url('resource.page.index'),
icon: 'Home'
},
{
caption: 'Articles',
key: 'article',
url: router.url('resource.article.index'),
icon: 'Copy'
},
{
caption: 'Misc',
icon: 'ThreeDots',
subItems: [
{
caption: 'Categories',
key: 'category',
url: router.url('resource.category.index')
},
{
caption: 'Tags',
key: 'tag',
url: router.url('resource.tag.index')
}
]
}
],
getUserNavigationItems: router => [
{
caption: 'My settings',
url: router.url('mySettings'),
appLink: true
},
{
caption: 'Public pages',
url: 'http://mySite.com'',
newTab: true
},
{
caption: 'Show search',
action: mainNavigation => mainNavigation.showSearch().close()
}
],
getProjectCaption: () => 'Trikoder CMS',
getUserCaption: () => 'Demo user'
};
Method "getNavigationItems" takes array of objects with following keys:
- caption: for item caption
- key: prefix for "Link" class name
- url: url to point to
- icon: adds sufix to element icon classname (all current icon suffixes can be found in "/src/scss/library/_variables.scss" file, under $icons variable)
Method "getUserNavigationItems" takes array of objects with following keys:
- caption: for item caption
- url : url to point to
- action: if url is ommited, application calls this function with mainNavigation as argument
- appLink: Boolean value, true is in app link
- newTab: adds target="_blank" attribute to link
- icon: adds sufix to element icon classname (all current icon suffixes can be found in "/src/scss/library/_variables.scss" file, under $icons variable)
Use method "getProjectCaption" to set CMS project name.
Use method "getUserCaption" to set current user caption.
Application object is glue that ties all CMS components and services together. It is used to connect services and routes, load translations, inject boot (config) data and start application.
import app from 'trim/app.js';
import translations from 'trim/lang/english.js';
import routes from './routes.js';
import services from './services.js';
import './scss/main.scss';
app
.loadTranslations(translations, 'en')
.registerServices(services)
.registerRoutes(routes)
.start();
Used to register user defined services to service container.
Used to register user defined routes to route registry.
Used to import translation data for specific locale.
Used to set application locale.
app.setLocale('hr'); // en by default
Used to get application locale.
app.getLocale() // en by default;
Used to set Promise
function before creating admin instance.
app.beforeAdminEnter(() => { return Promise.resolve(); });
Once called application will setup router, services and main view components.
Trim based application is configured by setting boot (or config) data in your main entry point.
Anything can be inserted in boot data storage, only baseUrl
and baseApiUrl
are mandatory.
app.setBootData({
baseUrl: process.env.BASE_URL,
baseApiUrl: process.env.BASE_API_URL
})
Configure Trim application to use browser history API.
app.setBootData({
usesPushState: true
})
API adapter can be instructed to use PATCH
insted of PUT
method when updating JSON:API resources.
app.setBootData({
usePatchForUpdate: true
})
JSON:API resource URL slugs can be customized via resourceToApiMap
config property.
Used this when JSON:API resource type is not directly mapped to resource API URL.
app.setBootData({
resourceToApiMap: {
article: 'articles'
user: 'users'
}
})
Trim comes with offset (default) and page based pagination strategies included.
Customize offset based strategy (creates API query like ?page[offset]=0&page[limit]=15
):
app.setBootData({
apiPagination: {
strategy: 'offsetBased',
offsetKey: 'offset',
limitKey: 'limit'
}
})
Set and customize page based strategy (creates API query like ?page[number]=1&page[size]=15
):
app.setBootData({
apiPagination: {
strategy: 'pageBased',
numberKey: 'number',
limitKey: 'size'
}
})
Configure error message lookup field when resource model has validation errors.
app.setBootData({
validationErrorField: 'detail'
});
app.setBootData({
toggleColumnsVisibility: true, // activate toggle list table columns visibility feature, default is FALSE
itemsPerPage: 15, // default number of items per page
googleMapsApiKey: '123123', // API key for google maps
ckEditorPath: 'https://cdn.ckeditor.com/4.10.0/standard-all/' // ckeditor CDN
})
Boot data can later be retrieved like so:
import bootData from 'trim/library/bootData.js';
bootData('baseUrl'); // outputs boot data baseUrl value
To authenticating users to your app you have to implement simple authentication driver. Default view for authenticating with username and password is included in Trim. Examine base auth API for full implementation details. Simple driver implementation is shown bellow:
import api from 'trim/library/api.js';
import auth from 'trim/library/auth.js';
export default auth.extend({
isUserLogged() {
return Boolean(localStorage.getItem('accessToken'));
},
loginWithCredentials(credentials) {
return new Promise((resolve, reject) => {
if (credentials.username.length && credentials.password.length) {
localStorage.setItem('accessToken', 'testToken');
resolve();
} else {
reject(new Error('Login failed'));
}
});
},
onAuthorization() {
api.defaults.headers.common.Authorization = 'Bearer ' + localStorage.getItem('accessToken');
},
onDeauthorization() {
localStorage.removeItem('accessToken');
delete api.defaults.headers.common.Authorization;
}
});
Inform application that you want to use auth driver you implemented:
// app.js
import auth from './auth.js';
...
app.useAuth(auth);