The invisible Content Management System

GlintCMS is a modular and flexible CMS that basically has no backend, but slick inline ediiting capabilities. It is built with node.js

TL;DR

GlintCMS is an in-page, overlay style WYSIWYG (What you see is what you get) CMS. What this means is that to edit a page you simply navigate to the page as you would when you browse your website. Once you arrive at a page you use the overlaid main menu to perform tasks or you simply hover over the main section of the page to edit content or change page settings.

The building blocks of GlintCMS are designed that you can build Web-Sites as well as Web-Applications.

general

extendability

The following api document describes how to "use" the building blocks, (not how to extend them).

naming/require

The building blocks are called: wrap, container, etc. in this document. However, the module names to require them start always with glint-.
Also in general the module names are hierarchically structured, separated with a dash -, where on the left hand side is the more generic part of the name.

So if you want to use the adapter with the ajax provider for example, you do it like this:

get/set

methods

The building blocks methods emit events for every method with the given name:

emit(pre-<methodName>, arguments)

emit(<methodName>, arguments)

emit(post-<methodName>, arguments)

rendering/place

rendering (load) is done by default on the server.
editing, saving and deleting is always initiated in the browser.
blocks and widgets can be defined to render in the browser when needed.
however you can also override where the components (blocks and widgets) are rendered all together.
you can use this for example to let everything be rendered on the server,
when the site is being called by a bot, search engine, crawler or the like.

place
glint is designed to use on the server as well as in the browser (pick you buzzword for it... universal, polymorph, what ever you like).
With the place get/set method, you can define, where you would like the control to be rendered.

These are the available options (strings):

server

browser

both

force:server

force:browser

force:both

priorities

(0:low priority ... 3:high priority)
0 render on server by default
1 Block.render('browser') or
Widget.render('browser')
-> render these items in the browser
2 Wrap.render('server') or
Container.render('server')
-> render ALL items on the server, e.g. when requested by a search engine.
3 SpecificBlock.render('force:both') or
Widget.render('force:both)
-> when a Specific Block has this flag, it will always be rendered on both sides (server and browser)
4 Same as priority 3 but with 'force:server' or 'force:client'
-> render always on the server respectively in the browser

wrap

A wrap can hold several containers and widgets as well as other wraps.
Let's call them controls. With the wrap, you can define how the controls are loaded.
First you can define the default object with the method defaults.
With the methods parallel, series and eventually, you can define everything from a simple to a quite sophisticated loading execution.
See flow-builder

The wrap runs on the server and/or in the browser depending on the defined places.

properties

api

the api property must not be overwritten.

Wrap.api === 'wrap'

get/set

You can get/set these properties on the wrap.

key

id

selector

prepend

append

el

place

editable

cid

When you set editable or place on the wrap, the value is also set on all of it`s controls.

cid gets/sets the id on the first container that the wrap holds.

methods

constructor

// the `wrap` has got the optional key, control arguments.
// what's added in the constructor get's added with the `parallel` workflow.
var wrap = Wrap(key, control);
// but you can also create the Wrap fist, and then define your workflow.
// in this example the articles and projects `load in parallel` and after they are done,
// the resulting transfer object is handled over to the next steps:
// 1. in this exampe it's first the contentWidget,
// 2. and then the layoutWrap after the previous step is done.
var wrap = Wrap();
wrap
.parallel(container)
.parallel('articles', articles.selector('.js-articles'))
.parallel('projects', projects.selector('.js-projects'))
.series('content', contentWidget.place('force:server'))
.series(LayoutWrap(o.layout).place('force:server'))

defaults

It let's you define a default object, that's the starting object in every load workflow.
This object is the initial workflow transfer object

parallel

When you define several controls with the parallel method right after each other, they get executed in parallel, and only when all of them have finished (or one of them has an error), the next step is executed.

// adds a single control, the clone of the resulting object merged with the transfer object.
wrap.parallel(control;
// adds a sincle control, the clone of the resulting object is insterted into the transfer object with the given `key`.
wrap.parallel(key, control);
// examples
wrap.parallel(container);
wrap.parallel('news', widget1);
wrap.parallel('articles', widget2);

series

series method calls, are executed after the previous method has finished.
It has got the same signature as the parallel method.

eventually

eventually method calls are started immediately, but are evaluated only at the very end when the whole workflow has finished.
It has got the same signature as the parallel method.

data

You can optionally provide a data method, if your widget need's to make asynchronous data calls.
It returns this and takes a callback function with the parameters: callback(err, result).

widget.data(callback);

render

The render function is a synchronous function and must return the rendered content.
The data object is provided in the first argument.
It let's you choose what ever rendering engine you want to use (as long as it runs on the server as well as in the browser).

load

The load method is called from the wrap that contains this widget during the load workflow execution.
Most probably, you never have to call this method directly.
Internally, the load method calls the data method (if it was provided), and then render with the resulting object.

container

Containers are only used when you use blocks.
A container runs on the server and in the browser. On the server, only the load method is called (most likely from the surrounding wrap),
In the browser, there is more methods:

save

aaand finally it calls the load method on this container to finish the command.

cancel

(runs only in the browser)

Internal sequence:

it basically just calls the load on this container.

delete

(runs only in the browser)

Internal sequence:

it first calls the adapters delete method,

and afterwards it calls the load on this container, to finish things up.

block

The blocks are the heart of everything that is editable in GlintCMS.

properties

api

the api property must not be overwritten.

Block.api === 'block'

get/set

You can get/set these properties on the block.

id

selector

el

place

methods

constructor/methods

// you normally instantiate the `block` with the specific `block-provider` that it should hold
var block = Block(blockProvider);
// Example:
var block = Block(TextBlock()).use(Style());
// however you can also add/remove the `block-provider` later with `delegate` and `undelegate`
var block = Block();
block.delegate(TextBlock());
// you can also undelegate a block
block.undelegate(textBlock);

The block basically delegates the method calls to the specific block-provider.
Due to a runtime behaviour (missing el "HTMLElement"), the block has the ability to buffer method calls in a FIFO, and execute them later.

In addition to the 'getters and setters', it buffers and forwards the following methods:

load

edit

save

cancel

hasChanged

isValid

plugins

You can extend blocks with plugins with the use method, as well as with the mixin method.

use

Consult the [extend] documentation or the code.

mixin

Consult the [extend] documentation or the code.

// Example:
var block.use(Style());

adapter

The adapter is the interface to the storage.

properties

api

the api property must not be overwritten.

Adapter.api === 'adapter'

get/set

You can get/set these properties on the adapter.

db

type

fn

methods

constructor/methods

// you normally instantiate the `adapter` with the specific `adapter-provider` that it should delegate it's calls to.
var adapter = Adapter(adapter);
// Example:
var adapter = Adapter(AjaxAdapter());
// however you can also add/remove the `adapter-provider` later with `delegate` and `undelegate`
var adapter = Adapter();
adapter.delegate(AjaxAdapter());
// you can also undelegate a block
adapter.undelegate(ajaxAdapter);

The adapter basically delegates the method calls to the specific adapter-provider.

find(query, callback)

load(id, callback)

save(id, content, callback)

delete(id, callback)

The function has the form: callback(err, result)

the result object is the parsed javascript object for the given id,
and an array with the matching objects on the find callback.

plugins

You can extend adapters with plugins with the use method, as well as with the mixin method.

use

Consult the [extend] documentation or the code.

mixin

the mixin is mainly used to extend the adapter's query capability.
Since the adapter does not "magically unify the different storage query language", but exposes it directly,
the adapter needs a way to handle the different provider's queries.

As you can see in the example, the mixin must have an object, with the adapter-provider name as the key,
and the functions to mixin as the value (nested object).

This way you can support more than just one adapter-plugin,
and if you use some one else's module that does not have the queries for your provider,
just add them yourself and send a pull request.

// Example:
var block.use(Style());

trigger

A trigger consumes the containers api methods and exposes them to the user in a usable form.
Therefore triggers are only useful in the browser, not on the server side.
Although theoretically, you could write a trigger to use on the server side as well, maybe for application integration.

extend glintcms

It was important during the design of the module's api, to come up with something that's easy to extend.
And I think you can say, it is a strength of GlintCMS, that it is very flexible and easy to extend.

providers

If you want to integrate third party modules for example for a new editing experience, you can just create a new block-provider, and implement the methods:

mixin

If the plugin is just a function, or a couple of functions, you can insert them with the mixin mechanism.
It takes an object with the required functions as key, value pairs, and it returns this, to make it chainable.

events

module development

Developing modules for glint is not difficult. It does not force you using specific libraries, or patterns.

However there is a few things you should consider:

general considerations

make sure your module runs on the server as well as in the browser (if it is not intended otherwise).

The main glint building blocks right now don't use ES2015, ECMAScript 6 or ES6 or how ever you call the latest JavaScript version.
This helps to keep things simple and avoid running into compatibility pitfalls especially with the different browsers.

it is often better to keep the modules simple for every one, we don't use anything that needs to be compiled like e.g. less, sass or coffeescript.

if you still use something that needs to be compiled, anything like e.g. less, sass or coffeescript, do it in your build, and provide the compiled js, css.

also your module should be minimal and not depend on other large modules.

know what you require('...')

substack wrote a really helpful guide, how you can develop components with browserify: browserify-handbook

structure

there is no restrictions on how to structure a glint module

the good thing about small modules is, you don't need to care about structure, but size

try to split it up into different modules, when it becomes too big

a flat folder structure will do most of the time

common modules

If you can, it makes sense to depend on a few common modules instead of many different ones.
This list contains modules (dependencies) that are used in many of the modules: