Skip to content

Code conventions

Christopher Joel edited this page May 4, 2019 · 17 revisions

<model-viewer> strives to be consistent with commonly accepted code conventions and formatting best practices. To this end, we have documented many of our conventions as tooling configuration. Currently, we use the following tools to enforce some formating and style rules:

  • clang-format
  • tsc (the TypeScript compiler)

We are also looking into adding tslint to this list. If you know of tools that we can use to automate checks anything that is considered conventional, please let us know! We are eager to document as much of our conventions as possible with tools.

Please ensure that you have your local environment set up to take full advantage of the tools above! Many of us use plugins (in Vim and VSCode) to auto-run these tools as we are developing. There is a good chance that your editor of choice has such plug-ins available as well.

Project-specific guidelines

There are some code conventions that are project-specific and not yet enforced with tools, or not easily enforced with tools. To the extent possible, we would like to enforce all conventions with tools, but until we can we will document them here for every contributor's reference.

API access

Standard JavaScript has no first-class mechanism for scoping API access. So, to the extent that it is ever done, it is always enforced as a convention. This section documents our project's convention.

API in <model-viewer> has three meaningful access levels:

  • public: can be invoked by end-users of the library
  • private: strictly internal to the class, mixin or module that implements it
  • protected/shared: accessible by subclasses, importing modules and tests

Public API is API that fits any of the following criteria:

  • It is a statically assigned export of a module
  • It is considered public API according to the LitElement documentation
  • It is a bare property defined on a class or object, and not considered private/protected API according to the LitElement documentation

Private API must:

  • Use the private TypeScript access modifier where applicable
  • Be assigned to a JavaScript Symbol computed property
  • Assign its Symbol to an identifier that begins with $
  • Not directly export the Symbol from the module where it is created

For example:

const $privateMethod = Symbol('privateMethod');

export class CoolClass {
  private [$privateMethod]() {
    // Private implementation here
  }
}

For protected/shared API, all of the rules for private API apply except:

  • It uses the protected TypeScript access modifier where applicable
  • The Symbol must be directly exported from the module where it is created

For example:

export const $protectedMethod = Symbol('protectedMethod');

export class CoolClass {
  protected [$protectedMethod]() {
    // Shared implementation here
  }
}

License headers

With a few exceptions, all source code should contain the appropriate license header at the top. Please ensure that the current year is used when adding a license header to a new source file!

For JavaScript/TypeScript/CSS, the license header looks like this:

/*
 * Copyright 2019 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

For HTML, the license header should look like this:

<!--
/*
 * Copyright 2019 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
-->

For shell scripts, the license header should look like this:

##
# Copyright 2019 Google Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

Shared implementation

We take advantage of a dynamic inheritance pattern that is somewhat unique to JavaScript in order to break our implementation up into a series of independent mixins. A basic example of this pattern in plain JavaScript looks like this:

// In cool-mixin.js:
export const CoolMixin = (SuperClass) => class extends SuperClass {
  coolMixinMethod() {
    console.log('World');
  }
};

// In another module:
import {CoolMixin} from './cool-mixin.js';

class BaseClass {
  constructor() {
    console.log('Hello');
  }
}

class CoolClass extends CoolMixin(BaseClass) {}

const cool = new CoolClass();
cool.coolMixinMethod();

Wherever possible, API that decorates the Custom Element implementation of <model-viewer> should be a part of a mixin that decorates the ModelViewerBaseElement class.

For cases where API needs to be shared between mixins, it should happen in one of the following ways:

  • The shared implementation should manifest as CustomEvents dispatched from the element
    • Note that events dispatched on the element always constitute public API
  • The implementation should be added to ModelViewerBaseElement

Visible UI elements

Whenever we add visible UI elements to <model-viewer>, they automatically inherit the use case that they should be customizable or removable by users.

For all UI elements, users should always have the following two options:

  1. Customize built-in UI styles (to the extent that it makes sense) via CSS custom properties (and someday also CSS shadow parts)
  2. Bring your own UI via Shadow DOM content projection

You can see an example of Option 1 in the PR description of https://github.com/GoogleWebComponents/model-viewer/pull/487 where we added CSS custom properties for styling the progress bar and poster.

With Option 1, we make it easy to rebrand the built-in UI by customizing details like color, font face, icons and perhaps paddings, margins and font sizes in some cases.

You can see an example of Option 2 in the PR description of https://github.com/GoogleWebComponents/model-viewer/pull/322 where we added the interaction prompt.

Option 2 is a more robust approach to customization. It allows the content author to provide complex, bespoke alternatives to the built-in UI.

Clone this wiki locally