Skip to content

Instantly share code, notes, and snippets.

@moagrius
Last active March 18, 2016 15:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moagrius/124ac4dc7cb615a9371e to your computer and use it in GitHub Desktop.
Save moagrius/124ac4dc7cb615a9371e to your computer and use it in GitHub Desktop.

Composer

Composer UI interacts with the Python WSGI app at https://github.com/dailydot/composer to manage article content.

Rather than allowing markup to structure content, Composer uses the concept of Nodes. A Node represents a piece of content, such as a paragraph, an image, a heading, a list of text items, or more sophisticated third-party content sources called embeds.

At the top level, Composer uses a dot.cms.editor.Editor instance to manage dot.cms.editor.articles.Article instances. An Editor instance will manage a single Article instance at a time, but that Article instance can be replaced, removed, or recreated using the same Editor instance.

Article instances describe the specific article, including fields for properties like title, summary, author, creation or modification dates, etc. The content of the article is described as a list (array) of Nodes, in this package represented by dot.cms.editor.articles.ArticleContentNode instances.

A client application will receive an article (or articles) with the properties described above; it is the responsibility of that client application to display the article using that information. For example, a web browser might display a list of articles using a thumbnail image, the tile, and a summary. When drilling down to detail state for a single article, a web browser might display a paragraph using a <p> tag, but also might decorate that tag with CSS class names, inline styles, data attributes, etc. After each <p> element, the client might choose to render another decorator element, such as an icon or horizontal rule. Contrast this with an XML feed - a paragraph might remove any formatting instructions within the paragraph and print out <item> elements. A web client might display related content within an inline-frame, but that same content might be represented by a link or button in a mobile application.

Installation

Include all JavaScript files in the dot directory of this repo.

There is an example implementation in the /app/ directory of this repo. This should work out of the box, assuming the Composer backend is running on port 5000 (otherwise, just update the url variable in /app/assets/scripts/application.js).

The API Key and Client ID used for the Google Document selection UI should be replaced with a property owned by the Daily Dot; the current configuration is from a personal account.

Setup

Create an Editor instance:

    var editor = new dot.cms.editor.Editor();

Register Node classes the editor should be able to handle:

    editor.registerNodeType(dot.cms.editor.articles.nodes.ParagraphNode);
    ...

Initialize it with URL to Composer backend, and a map of DOM element needed to display the UI:

    editor.initialize('http://path.to/REST/API/', {
      controls : document.getElementById('cms-editor-add-nodes'),
      form : document.getElementById('cms-editor-form'),
      preview : document.getElementById('cms-editor-preview'),
      toggle : document.getElementById('cms-editor-toggle-button')
    });

The editor can create new Article instance, or show (and edit) existing Articles. How those articles are provided to the editor is up to the author (e.g., a select box, a list of links, a GET parameter, etc); an article can be loaded by its UUID with the fetchArticleById method. Changes are saved using the saveArticle method.

REST API Interactions

To create a new article:

    editor.initializeNewArticle();

To load an existing article into the editor, by UUID:

    editor.fetchArticleById(id);

To reload the currently displayed article:

    editor.fetchArticle();

To save the currently displayed article to the server:

    editor.saveArticle();

To set the current article values from an object (probably the parse of a JSON string from the server):

    editor.parseArticle(description);

To set the current article values from a JSON string:

    var description = JSON.parse(jsonString);
    editor.parseArticle(description);

There is not an editor API to get a list of existing articles, but it can be accomplished easily with native methods:

    var xhr = new XMLHttpRequest();
    xhr.addEventListener('load', function(e){
      var json = JSON.parse(e.target.responseText);
      // you may want to do some validation here,
      // e.g., test for json.error, or ensure json.data exists, etc
      var articles = json.data; // this is an array of article instances
    });
    xhr.open('GET', url);
    xhr.send();

Importing; Translating HTML to structured content

The dot.cms.editor.importer.MarkupToNodeConverter class contains functionality to convert arbitrary HTML blobs into node lists, generally either from a loaded document, or from a paste event. This happens in (roughly) two passes: the first normalized the HTML to a predictable structure, and removes markup that is not relevant or not supported; the second pass consumes that normalized structure and mutates a list of Nodes.

A string of HTML passed to a MarkupToNodeConverter's convert method will be normalized and each child passed to each processor function registered with the converter:

    var converter = new dot.cms.editor.importer.MarkupToNodeConverter();
    converter.registerProcessor(yourProcessorFunction);
    ...

The processor function is passed information about the DOM node, and access to the converter instance and the converter's existing ArticleContentNode list. The processor can react in any way the author wants, but generally the element is inspected for potential Nodes, and the Node list is mutated. Once all processors have been executed, the current article's Node list can be updated with dot.cms.editor.articles.Article.setNodes method.

Extending

To add a new node type:

  1. Get latest composer-frontend repo from https://github.com/dailydot/composer-frontend.

  2. Create a class that inherits from dot.cms.editor.articles.ArticleNode (see jsdocs in class definition).

  3. Add rule in editor.css for toolbar icon:

    .<add-node-selector> {
      background-image: url(../images/name-of-icon.png);
    }
    
  4. Add rule in editor.css for drag hint:

    .<add-node-selector>.gu-transit:after {
       content: "Add <Human Readable Label of Node> Here";
    }
    
  5. Add rule in editor.css for tag:

    .cms-editor-node-wrapper-<slug>-node:before {
      content: '<Human Readable Label of Node>';
    }
    
  6. Register the node with the editor instance:

  7.  editorInstance.registerNodeType(dot.cms.editor.articles.nodes.YourNewNodeType);
    
  8. Create and register a processor function for importation (see jsdoc for dot.cms.editor.importer.MarkupToNodeConverter for signature):

    markupToNodeConverterInstance.registerProcessor(yourProcessorFunction);
    
  9. In tests, create a file with Jasmine unit tests describing your new node.

  10. Get latest composer repo from https://github.com/dailydot/composer.

  11. In nodes.py, define your class:

    class YourNewNodeType(Node):
    
        __typename__ = 'your-new-node-type'
    
        def __init__(self, **kwargs):
            self.some_arbitrary_property = fields.HTMLField()
            super(YourNewNodeType, self).__init__(**kwargs)
    
  12. In node_tests.py, create unit tests:

    class YourNewNodeTypeTests(TestCase):
    
        def test_typename(self):
            self.assertEqual(nodes.YourNewNodeType.__typename__, 'your-new-node-type')
    
        def test_create(self):
            n = nodes.YourNewNodeType(some_arbitrary_property='It was a dark and stormy night...')
            self.assertEqual(n.some_arbitrary_property.get(), 'It was a dark and stormy night...')
    
@arsenio
Copy link

arsenio commented Mar 16, 2016

This is really clear and helpful documentation. One thing I'd add is a shoutout to the reference implementation in app/ (which you may want to move to a more descriptive subdirectory like example or reference once you're done).

@grobertson
Copy link

Very clear on a first read.

@jnovinger
Copy link

Quick question re: adding a new node type in Javascript. You mention adding a couple style selectors/rules to editor.css for each new node type. Rather than having the author edit things in multiple places, would it make sense to allow those rules to be added as attributes on the new node class and then added to the CSS programmatically?

Edit: I'd also argue that this presents a simple way of providing some default options for those things like icons or tool tip text.

I can imagine this is less than optimal from a page render-time point of view. However, it does strike me as a slightly more developer friendly way of getting started with a new node type.

Take the above as you will, I realize this is supposed to be comments on the documentation itself, not the API design. :D

Otherwise, I only see a couple minor spelling/style issues:

@moagrius
Copy link
Author

@arsenio will do, thanks
@grobertson 👍 thanks for looking
@jnovinger good question, and something I considered. i decided against it for a couple reasons: first, we can consider all those pieces "presentational", and therefore appropriate in the style rules, and second because moving it form CSS to JS would mean more JS configuration, which i'm not sure is better or worse, but for presentational elements, i'd rather have the updates made to the CSS. will do on the syntax stuff 👍

@ajferrick
Copy link

I think this is very clear documentation. Is there nothing that needs to be said about the REST API interactions?

@moagrius
Copy link
Author

@ajferrick will do, thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment