Writing a Component
Image by Ruben Mueller from vrjump.de
This guide will take it pretty slow. We recommend skimming over the Component API documentation before going through this guide as that documentation will be more concise. Note that components should be defined before
We’ll go over examples on writing components. The examples will do mostly trivial things, but will demonstrate data flow, API, and usage. To see examples of non-trivial components, see the Learning Through Components in Ecosystem section.
Table of Contents
- Example: hello-world Component
- Example: log Component
- Example: box Component
- Example: follow Component
- Learning Through Components in Ecosystem
- Publishing a Component
Let’s start with the most basic component to get a general idea. This component will log a simple message once when the component’s entity is attached using the
Components are registered with
.init(), which is called once when the component is first plugged into its entity.
In the example below, we just have our
.init() handler log a simple message.
Then we can use our
hello-world component declaratively as an HTML attribute.
Now after the entity is attached and initialized, it will initialize our
hello-world component. The wonderful thing about components is that they are called only after the entity is ready. We don’t have to worry about waiting for the scene or entity to set up, it’ll just work! If we check the console,
Hello, World! will be logged once after the scene has started running and the entity has attached.
Another way to set a component, rather than via static HTML, is to set it programmatically with
.setAttribute(). The scene element can take components too, let’s set our
hello-world component on the scene programmatically:
Similar to the
hello-world component, let’s make a
log component. It’ll still only just do
console.log, but we’ll make it able to
console.log more than just
Hello, World!. Our
log component will log whatever string its passed in. We’ll find out how to pass data to components by defining configurable properties via the schema.
The schema defines the properties of its component. As an analogy, if we think of a component as a function, then a component’s properties are like its function arguments. A property has a name (if the component has more than one property), a default value, and a property type. Property types define how data is parsed if its passed as a string (i.e., from the DOM).
log component, let’s define a
message property type via the
message property type will have a
string property type and have a default value of
string property type doesn’t do any parsing on the incoming data and will pass it to the lifecycle method handlers as is. Now let’s
message property type. Like the
hello-world component, we write a
.init() handler, but this time we won’t be logging a hardcoded string. The component’s property type values are available through
this.data. So let’s log
Then from HTML, we can attach the component to an entity. For a multi-property component, the syntax is the same as inline css styles (property name/value pairs separated by
: and properties separated by
So far, we’ve been using just the
.init() handler which is called only once at the beginning of the component lifeycle with only its initial properties. But components often have their properties updated dynamically. We can use the
.update() handler to handle property updates.
Lifecycle method handlers. Image by Ruben Mueller from vrjump.de
To demonstrate this, we’ll have our
log component only log whenever its entity emits an event. First, we’ll add an
event property type that specifies which event the component should listen on.
Then we’ll actually move everything from our
.init() handler to our
.update() handler. The
.update() handler is also called right after
.init() when the component is attached. Sometimes, we have most of our logic in the
.update() handler so we can initialize and handle updates all at once without repeating code.
What we want to do is add an event listener that will listen to the event before logging a message. If the
event property type is not specified, we’ll just log the message:
Now that we’ve added our event listener property, let’s handle an actual property update. When the
event property type changes (e.g., as a result of
.setAttribute()), we need to remove the previous event listener, and add a new one.
But to remove an event listener, we need a reference to the function. So let’s first store the function on
this.eventHandlerFn whenever we attach an event listener. When we attach properties to the component via
this, they’ll be available throughout all the other lifecycle handlers.
Now that we have the event handler function stored. We can remove the event listener whenever the
event property type changes. We want to only update the event listener when the
event property type changes. We do this by checking
this.data against the
oldData argument provided by the
Now let’s test our component with an updating event listener. Here’s our scene:
Let’s have our entity emit the event to test it out:
Now let’s update our event to test the
Let’s handle the case where the component unplugs from the entity (i.e.,
.removeAttribute('log')). We can implement the
.remove() handler which is called when the component is removed. For the
log component, we remove any event listeners the component attached to the entity:
Now let’s test out the remove handler. Let’s remove the component and check that emitting the event no longer does anything:
Let’s allow having multiple
log components attached to the same entity. To do so, we enable multiple instancing with the
.multiple flag. Let’s set that to
The syntax for an attribute name for a multiple-instanced component has the form of
<COMPONENTNAME>__<ID>, a double-underscore with an ID suffix. The ID can be whatever we choose. For example, in HTML:
Or from JS:
Within the component, if we wanted, we can tell between different instances using
this.id would be
this.attrName would be the full
And there we have our basic
For a less trivial example, let’s find out how we can add 3D objects and affect the scene graph by writing a component that uses three.js. To get the idea, we’ll just make a basic
box component that creates a box mesh with both geometry and material.
Image by Ruben Mueller from vrjump.de
Let’s start with the schema. The schema defines the API of your component. We’ll make the
color configurable through the properties. The
depth will be number types (i.e., floats) with a default of 1 meter. The
color type will have a color type (i.e., a string) with a default of gray:
Later, when we use this component via HTML, the syntax will look like:
Let’s create our three.js box mesh from the
.init(), and we’ll later let the
.update() handler handle all the property updates. To create a box in three.js, we’ll create a
THREE.MeshStandardMaterial, and finally a
THREE.Mesh. Then we set the mesh on our entity to add the mesh to the three.js scene graph using
Now let’s handle updates. If the geometry-related properties (i.e.,
depth) update, we’ll just recreate the geometry. If the material-related properties (i.e.,
color) update, we’ll just update the material in place. To access the mesh to update it, we use
Lastly, we’ll handle when the component or entity is removed. In this case, we’ll want to remove the mesh from the scene. We can do so with the
.remove() handler and
And that wraps up the basic three.js
box component! In practice, a three.js component would do something more useful. Anything that can be accomplished in three.js can be wrapped in an A-Frame component to make it declarative. So check out the three.js features and ecosystem and see what components you can write!
Let’s write a
follow component where we tell one entity to follow another. This will demonstrate the use of the
.tick() handler which adds a continuously running behavior that runs on every frame of the render loop to the scene. This will also demonstrate relationships between entities.
First off, we’ll need a
target property that specifies which entity to follow. A-Frame has a
selector property type to do the trick, allowing us to pass in a query selector and get back an entity element. We’ll also add a
speed property (in m/s) to tell specify how fast the entity should follow.
.tick() handler will be called on every frame (e.g., 90 times per second), we want to make sure its performant. One thing we don’t want to do is be creating unnecessary objects on each tick such as
THREE.Vector3 objects. That would help lead to garbage collection pauses. Since we’ll need to do some vector operations using a
THREE.Vector3, we’ll create it once in the
.init() handler so we can later reuse it:
Now we’ll write the
.tick() handler so the component continuously moves the entity towards its target at the desired speed. A-Frame passes in the global scene uptime as
time and time since the last frame as
timeDelta into the
tick() handler, in milliseconds. We can use the
timeDelta to calculate how far the entity should travel towards the target this frame, given the speed.
To calculate the direction the entity should head in, we subtract the entity’s position vector from the target entity’s direction vector. We have access to the entities’ three.js objects via
.object3D, and from there the position vector
.position. We store the direction vector in the
this.directionVec3 we previously allocated in the
Then we factor in the distance to go, the desired speed, and how much time has passed since the last frame to find the appropriate vector to add to the entity’s position. We translate the entity with
.setAttribute and in the next frame, the
.tick() handler will be run again.
.tick() handler is below.
.tick() is great because it allows an easy way to hook into the render loop without actually having a reference to the render loop. We just have to define a method. Follow along below with the code comments:
There are a large number of components in the ecosystem, most of them open source on GitHub. One way to learn is to browse the source code of other components to see how they’re built and what use cases they provide for. Here are a few places to look:
- A-Frame Registry - Curated community components.
- A-Frame core components - Source code of A-Frame’s standard components.
- awesome-aframe components - Giant list of community components.
- A-Painter components - Application-specific components for A-Painter.
Many components in practice will be application-specific or one-off components. But if you wrote a component that could be useful to the community and is generalized enough to work in other applications, you should publish it to the ecosystem via the A-Frame Registry and
For a component template, we recommend using
angle is a command-line interface for A-Frame; one of its features is to set up a component template for publishing to GitHub and npm and also to be consistent with all the other components in the ecosystem. To install the template:
initcomponent will ask for some information like the component name to get the template set up. Write some code, examples, and documentation, and send a pull request to the A-Frame Registry to get it featured! Follow the Registry guidelnes, we’ll do a quick code review, and then the community will be able to use your component, and hopefully send some helpful pull requests back if needed!