Skip to content
This repository has been archived by the owner on Mar 30, 2019. It is now read-only.

DevDocComponents

Pomax edited this page Sep 22, 2014 · 6 revisions

Information for Developers

The appmaker project consists of three parts:

  1. ceci, an implementation of Custom Elements, based on Polymer, defined in the ./public/ceci directory.
  2. A collection of predefined elements (called "bricks" or "components" in most technical discussions relating to appmaker) located in the ./public/bundles/components directory
  3. An app-making single-page designer webapp, with front-end code located in the ./public/designer directory, and backend code located in the usual places for an express app.

Components and their build-up

Appmaker components, or bricks, are HTML5 Custom Elements defined using the Polymer framework, and enriched with some of Appmaker's ceci framework functions.

Appmaker components all live in the ./public/bundles/components directory, and each component lives in its own directory, named component-...; the button component lives in "component-button", the header component lives in "component-header", etc.

Each component has to have at least a ./component.html file, for its definition, and a ./locale/en-US.json file for defining all the component's localizable strings.

component.html

<polymer-element name="ceci-..." attributes="..." extends="ceci-element">
  <ceci-definition>...</ceci-definition>
  <template>
    <link rel="stylesheet" href="component.css"></link>
    <!-- actual HTML code that defines the internal skeleton -->
    <shadow></shadow>
  </template>
  <script>
    Polymer('ceci-...', {
      ready: function () {
        this.super();
        // your code goes here
      },
      attached: function () {
        this.super();
        // your code goes here
      },
      ...more yourelement.prototype functions go here...
    });
  </script>
</polymer-element>

the ceci definition

The ceci definition houses all the particulars about which functions a component supportsa and which attributes can be controlled through the designer. It's a JSON definition that looks as follows:

{
  "tags": ["...", ...],
  "thumbnail": "./thumbnail.png",
  "description": "...",
  "name": "...",
  "broadcasts": {
    ...
  },
  "listeners": {
    ...
  },
  "attributes": {
    ...
  }
}

tags

The component tags are an array of strings that allows the Designer to search for components better than if there are no tags. These tags are not related to the categories that bricks will show up under in the designer's component tray (which are regulated by the designer's component-tray.js.

thumbnail

You component can have a thumbnail, which we try to keep named thumbnail.png. If there is no thumbnail your component will show up with a placeholder thumbnail image instead.

description

This is a human readable, textual description of what your component is for. It's the "help text" for when users want to find out how to use your component so no technical details here, please.

name

The component name. This is usually the component's element name, without the "ceci-" prefix.

broadcasts

This section defines all the broadcast functions that a user will be able to make use of in their app for this component. The broadcast structure is a series of key:object bindings, with each key representing a broadcast function:

{
  "click" : {
    "label": "Press",
    "description": "Occurs when this brick is clicked."
  },
  "sendTime" : {
    "label": "...",
    "description": "..."
  },
  "...": {
    ...
  }
}

Each function will show up in the broadcast menu when using the designer, and can be assigned a channel to send out on by the user. You can also specify that a broadcast should be automatically assigned a channel, or even which channel it should use:

{
  "click" : {
    "label": "Press",
    "description": "Occurs when this brick is clicked.",
    default: true
  }
}

using true as a default will let the designer pick the next available broadcasting channel as broadcast channel for this function.

{
  "click" : {
    "label": "Press",
    "description": "Occurs when this brick is clicked.",
    default: "A"
  }
}

With a specific channel name, rather than the boolean true, the designer will bind the component in a way that uses that specific channel unless changed by the user. Note that this is not necessarily a good idea as the designer channel names have been known to change in the past, and there is every reason to expect this to happen again in the future.

listeners

Just like broadcasts, we can specify which listeners a component supports, so that data can make it into the component for processing. The structure is identical to the broadcasting structure, with the same optional channel defaults.

Note that these listeners are intended for performing "operations" in the component, rather than setting values for component attribute values. For those, the attributes structure has special instructions, described in the next section.

attributes

The attributes section defines which designer-controllable attributes this component supports, and which data type they are (so that the designer can inject the correct editing HTML code for it). Let's look at a part of the attributes definition for the ceci-header component:

{
  "header": {
    "label": "Label",
    "description": "Text for header.",
    "editable": "text", 
    "listener": true
  },
  "textalign": {
    "label": "Text Alignment",
    "description": "Text Alignment",
    "editable": "select",
    "options": {"Left":"Left","Center":"Center","Right": "Right"}
  },
  ...
}

Like the broadcasts and listeners, the structure is key:object definition pair, with each key being an attribute for the component, having a human readable label and description. Each attribute also has an editable property that tells the designer what kind of attribute it is, with the list of supported types defined in the ./public/designer/js/editable.js file. Currently this list is:

  • select: gives the user a selection with preset options. These options are supplied in the attribute definition using the options key (as seen above for the textAlign attribute).
  • text: gives the user a text field for editing the attribute value.
  • collection: a special data type that lets an attribute control a stored collection. Collections are explained in more detail in their own section.
  • textarea: gives the user a multiline text area to edit the attribute value with.
  • number: gives the user a number spinner for setting the attribute value. Numbers are restricted by min, max, and step properties, which define the lowest and highest number, as well as the increment/decrement value that the spinner uses to increase or decrease the number.
  • range: gives the user a slider for controlling the attribute. A range must have a min, max and step property before it makes sense.
  • boolean: gives the user a checkbox to regulate whether the attribute is "true" or "false"
  • color: gives the user a color picker for chosing colors. The chosen color will be stored in hex format (e.g. red is #FF0000, magenta is #FF00FF, etc)
Collections

this section should be stubbed out by @ScottDowne

The template block

This is where the element's internal HTML structure is defined. Even though a component will show as an "atomic" thing, existing either in whole or not at all, internally the component can be made up of an arbitrary collection of HTML elements (including other custom elements). For instance, this is the template for the "textbox with confirm button":

  <template>
    <link rel="stylesheet" href="component.css">
    <div id="wrapper">
      <input id="inputText" type="text" on-input="{{input}}" on-keypress="{{keypress}}" />
      <div id="button" style="color: {{textcolor}}; background-color: {{buttoncolor}};"
           on-ceci-pressup="{{pressup}}" on-ceci-pressdown="{{pressdown}}">
        <div class="bottom"></div>
        <div class="button-overlay"></div>
        <div id="buttonlabel" class="button-label">{{buttonlabel}}</div>
      </div>
    </div>
    <shadow></shadow>
  </template>

Here we first see a stylesheet being included, which is usually a good idea to make sure that a component always looks the same no matter which browser loads it. Then, we see a <div class="wrapper"> which houses the actual HTML code for this component. In addition to the fairly standard HTML content, we also see some special notation:

  • on-ceci-pressup: an event that triggers both on touchup and mouseup input
  • on-ceci-pressdown: an event that triggers both on touchdown and mousedown input
  • {{ ... }} templating macros

Templating macros

Custom elements implemented using the Polymer framework can use {{...}} macros to synchronise values in the element as DOM element and as HTML source code. For instance, if we have {{ myval }} in the template source code, and when we use this element "for real", we set element.myval = "cats", then Polymer will replace the {{ myval }} part in the source with the value cats instead. This is particularly useful for making sure strings and attribute values stay synchronised without having to worry about accessing and modifying them yourself.

The shadow DOM

The last element in the template is a placeholder for the shadow dom. Usually you won't do anything other than simply define it, and the Polymer framework in collaboration with the browser will make sure the right thing happens. Leaving it out, however, can produce unexpected results, so always make sure to include it.

The Polymer <script> section

After the template section, we got to the Polymer call that turns this component.html into a real custom element. Let's look at another example, this time the ceci-image element:

<script>
Polymer('ceci-image', {
  ready: function () {
    this.super();
    this.setAttribute("imageUrl", this.getAttribute("imageUrl") || "");
    this.img = this.$.image;
    this.img.style.height = this.getAttribute( "height" ) + "px";

    var that = this;
    this.onclick = function () {
      that.broadcast("click", this.img.getAttribute( "src" ));
    };
  },
  setValue : function(v) {
    this.setImageSource(v);
  },
  sourceChanged : function(oldValue, newValue){
    this.setImageSource(this.source);
  },
  setImageSource : function (val) {
    this.source = val;
    this.img.setAttribute('src', val);
  },
  heightChanged: function( oldValue, newValue ) {
    this.img.style.height = ( newValue || newValue === 0 ? newValue : "160" ) + "px";
  }
});
</script>

In order to register the component.html code as a real custom element, the Polymer('element name', { ... }) call registers the element into the browser's custom element registry, with the a prototype that's part specified by Polymer, and part specified by us.

One of the Polymer functions that we're tying into is the ready function, and because this is a Polymer function we need to make sure we also call the code that's already specified by Polymer (or, technically, any "parent" element). We do this by caling this.super(), which uses some Polymer magic to make sure the correct function gets called for us, after which our own code can run.

In this case, when the element is made ready for use it will set its image URL, and bind a this.image value. The this.$ is special shorthand notation that lets you grab elements by id from the <template> block. If there is an element <div id="teapot"> then using this.$.teapot will fetch that element for you inside the prototype definition functions, following the regular rules of javascript closures, so if the function context doesn't have this bound to the element, you need to make sure there's an alias that does, which we do for the ceci-image element by using that var that = this; line before we bind the onclick handler: when that function fires, "this" doesn't point to anything useful, so we need to make sure there's another variable that we can refer to that does.

We also see some functions that are entirely custom functions, like setValue, sourcechanged, setImagesource, and heightChanged. These are functions that we can automatically call by specifiying listeners for them, but we can also call them from anywhere in our prototype definition by using the usual this.setValue(...) etc.

Special Polymer functions that need this.super()

The list of functions that you can use but require you to call this.super() to make sure you're not preventing Polymer from doings its job are as follows:

  • ready: this is the general build function. Anything that needs to happen to an element before it can be inserted in to the DOM as useful element should be done here.
  • attached: this function gets called by Polymer when an element is inserted into any DOM, including non-document DOM fragments. Essentially, when this function is run, you know the element has been added to a DOM and might need to do things, but you can't be sure whether it's actually live on the page
  • domReady: this function is called when your element is added to the visible document. Unlike attached, domReady always gives you the guarantee that the element is being used on the page that the user is interacting with, rather than some DOM fragment that might never end up being used on the page itself.

There are more functions for which this.super() may be required, but these are the three important ones that we use in Appmaker. For more details, see the polymer documentation.

Special JavaScript calls when working with custom elements

Custom elements kind of live in two worlds, unoriginally called the "light DOM", which is what the user sees in terms of the source code on a page (like <ceci-image imageurl="..."></ceci-image> on their screen, and the "shadow DOM", which is what the element actually consists of internally. The general idea is that inside your custom element code, you never have access to the light DOM: you simply set properties on the shadow DOM and those changes will make it into the light DOM depeneding on which part of your element is currently visible. In order to make that a manageable proposition, there's a few things that make accessing the shadow DOM easier. First and foremost, there's the templating {{ macro }} syntax, for synchronising values. However, if that's not enough (for instance, you need to generate and/or kill off DOM elements inside your shadow structure) you can access everything inside your element using the shadowroot properties (like this.shadowRoots[0].querySelector, for instance).

Component.css

We can inline the styling for a component using a normal <style>...</style> block, but it's generally easier to use a dedicated component.css file for housing all the style rules. Appmaker will autobundle everything up into a nicely minifiable set anyway, so keeping styling separate makes the process of working on a component easier.

special CSS for custom elements

Officially, custom elements live in their own little bubble, their internals not affected by page javascript or styling, so in order to make sure that even when shimmed through Polymer styling shims these rules are preserved, custom element styling uses a special :host rule, explained best over on the polymer website

locale/en-US.json

Appmaker wouldn't be very interesting for the world if it was only in English, so all components in Appmaker are inherently localized by requiring there is a ./locale/en-US.json file, which specifies the English strings for a component that are to be used by the designer, as well as the published app. This file is also used as the basis for all other localisations, keeping the keys the same but having different values. As such, keys tend to not always be the full English strings, but simply short markers that "intuitively" resolve to their strings.

For instance, let's look at the locale file for the ceci-button component:

{
"ceci-button": "Button",
"ceci-button/description": "Responds to clicks and taps.",
"ceci-button/attributes/label": "Press Me",
"ceci-button/attributes/label/label": "Label",
"ceci-button/attributes/label/description": "Text shown on the button.",
"ceci-button/attributes/value": "Press",
"ceci-button/attributes/value/label": "Broadcast Value",
"ceci-button/attributes/value/description": "Value broadcast by the button when pressed.",
"ceci-button/attributes/textcolor/label": "Text Color",
"ceci-button/attributes/textcolor/description": "Color of the text on the button's label.",
"ceci-button/attributes/buttoncolor/label": "Button Color",
"ceci-button/attributes/buttoncolor/description": "Background color of the button.",
"ceci-button/attributes/buttonactivecolor/label": "Button Active Color",
"ceci-button/attributes/buttonactivecolor/description": "Background color of the button while it is being pressed."
}

Some of these strings pertain to the component itself, but most pertain to making sure the attributes are localised correctly. A minimal en-US.json will contain just the element's name as it should show up in the designer, and a description of it:

{
"ceci-custom-element-name": "Custom Name",
"ceci-custom-element-name/description": "A description for this component"
}

But most components that have editable attributes should also be localised, which generally take the form:

...
"ceci-custom-element-name/attributes/attributename": "Default value if applicable",
"ceci-custom-element-name/attributes/attributename/label": "Label for this attribute in the designer",
"ceci-custom-element-name/attributes/attributename/description": "A brief description of this attribute",
...