8 minutes read
The goal of this article is to describe the inner workings of an environment for WebVR components that’s based on A-Frame & Angular 2. As the design and maintenance of components like these are complex and repetitive tasks it’d be of much help to have a mechanism to offload the boilerplate like base structures, build-scripts, polyfills and other ‘usual suspects’ found in almost every web-oriented project. This is the main reason why this project exists. I wanted to have a tool-set that could not only help me create nice WebVR components but also deliver them easily by following best practices and accepted Web Standards.
The sources of this project can be found here.
A quick demonstration is located here.
This project follows the more or less ‘standard’ approach by semantically splitting the sources within the src/app-directory:
The initial logic is located in src/init-directory and contains main, vendor and polyfill scripts. As a build tool I’m using WebPack together with a few configuration files.
Booting the App
The main script loads the AppModule to boot the app.
But shortly before this happens the browser activates the A-Frame script by putting it before Angular’s Zone.js module.
This is mandatory as Zone.js takes over the document.registerElement function and expects all properties, like detachedCallback, to be set. More info on this issue can be found here.
The next step is registering predefined A-Frame modules that are located in app.loader. We define modules as objects that implement the IVrModule interface.
They carry information about their type, like ‘AFrame’, the markup that describes their structure and optional scripts. In fact, one could easily add new frameworks by creating further module enums.
This is an example of a simple module of type ‘AFrame’:
In a more realistic scenario such modules would be located in a separate storage. One could, for example, query them via REST-services and register on-the-fly. The module registration happens in vr-module.service that can also de-register existing ones. This service also dispatches messages to vr-module reducer.
This is how its registration method looks like:
The aforementioned reducer is based on @ngrx that will help us manage our application state more easily.
In its current state the reducer isn’t much used within the application because of a simple fact that in its original version the app functions only as a ‘show room’ for a few VR modules. Currently, its sole purpose is to keep the availableModules-property up to date which is bound to the modules-property of VrListComponent.
The store from above is being created here by using provideStore function from @ngrx.
I’m also not completely following the best practices because I don’t use any Action Creators. Instead, I’m dispatching raw messages. Please, forgive me. 🙄
In parallel to module registration the AppComponent builds up a proper view that’ll show us the modules in a sidebar. This part of the task is offloaded to another component, vr-list, that creates a list of module names. Notice that the surrounding bootstrap-structure could be safely replaced by any other styling library. Also take into account that data for [modules]-binding goes through an AsyncPipe that’s one of Angular’s default pipes. Previously we’ve selected an Observable from the vrModule–Store and therefore we have to properly consume those asynchronous data streams. That’s why AsyncPipe is made for. As our modules-property in VrListComponent knows nothing about Observables our data must go through this pipe first.
As shown in the above HTML we simply forward the contents of AppComponent’s availableModules property to VrListComponent’s modules property. If you don’t know the mechanics making this possible simply jump to this article of mine.
Additionally, we combine the EventEmitter from VrListComponent to AppComponent’s handler method onVrModuleSelected. We’re interested in user selections done on the Sidebar because we want to load the appropriate A-Frame module. Again, if you have problems understanding the mechanism you could read one of my previous articles first before going any further.
Now the most interesting part happens when user selections reach the event handler in AppComponent. Here, we’re not only creating a message object that describes the module but also utilizing Angular’s routing mechanism that’ll kick off VR-module instantiation itself.
By itself, our routing paths look very simple, because there’s only one possible target: VrWrapperModule.
But, there’s more as VrWrapperModule owns a separate routing definition that helps the framework load appropriate VR modules.
The routing target, VrWrapperComponent, subscribes to route-params Observable and reacts to changes by creating another carrier object called dynamicComponent. In case you need more info regarding Observables and functional/reactive coding in Angular, there’s an older article of mine that might be of some value to you.
Now, the question is, where does dynamicComponent go? Let’s see the HTML structure of VrWrapperComponent:
First, it takes care of removing any previous VR-related scripts from the document body. Then, it injects optional scripts that could be provided by the module. Finally, it creates future component’s metadata by copying dynamicComponent’s html-property.
Ultimately, we pass the metadata to a helper function called createComponentFactory that’ll complete our task by injecting a fully functional component into the DOM. The structure of this helper function is as follows:
First, we create a brand new @NgModule and feed it with CUSTOM_ELEMENTS_SCHEMA from Angular’s core library. This constant is very important because A-Frame uses it’s own components whose names begin with an “a-” prefix. Any HTML elements whose tags contain a dash in their name must either be “Angular-known” elements or there has to be a CUSTOM_ELEMENTS_SCHEMA loaded into containing @NgModule. Without this schema constant you’ll receive a ton of error messages complaining that there’s an “unknown element with name XYZ“.
Armored with this knowledge we go a step further an put the component declaration that contains our incoming metadata into @NgModule. As you can see above we’re just using an empty component class type, DnamicComponent, that served us as a wrapper to be combined with our previous metadata. With this new component we complete our @NgModule’s definition and hand it over to Angular Compiler’s method compileModuleAndAllComponentsAsync. As a result we get back a component factory (not a component!) that we’ll soon use to create a new component to be inserted into our view container.
As you can imagine, a Directive can’t have a template, so we have to find some ‘free space’ in the DOM where we can put our new component in. To prepare ourselves for this task we’ve already instantiated our Directive by feeding it a proper ViewContainer Reference via Angular’s Dependency Injection mechanism. Now, I hope, it’s easy to understand why we’re calling ViewContainerRef’s createComponent method and pass it our component factory.
The ViewContainer Reference should have no knowledge on how we create our components. It’s only duty is to provide us ‘some room’ for our new component. The rest is done in the factory itself.
Optional Scripts Injection
And just to make the journey complete, let’s examine the simple mechanism that helps insert optional scripts into the DOM.
We call NgZone’s runOutsideAngular to do some extra stuff in the DOM that should bypass Angular’s mechanism. In this case we’re directly manipulating DOM’s structure and because we don’t want to mess with Angular’s own DOM manipulation logic we do the job ‘outside’ of it.
We create a new <script> Tag and feed it with values that’ll help us recognize it later. This will be of great importance when we have to load another module. We don’t want old scripts lying around so we take care of giving them unique names. Ultimately, we insert them into the DOM and memorize in our internal activeScripts variable.
Have fun! 😀
9 thoughts on “WebVR with A-Frame & Angular”
Good article…….. also ends with one of the most complex config folders and package.json files I have seen to date! Cannot imagine how this would be maintainable within a dev group.
Unreal how much configuration is needed for a basic Angular AFrame project
The config and build-files from this article are pretty common in the web-dev/angular2 world. The original idea is from this project: https://github.com/AngularClass/angular2-webpack-starter
That original starter pack is …. let’s say not very intuitive.
It has, for example, 190+ open issues, and 780 closed issues – and that is just for the “hello world” type setup, there is no actual application logic in there. This is a serious concern with Angular projects, and something that thankfully the Angular CLI has addressed, where a basic (albeit feature rich) starter setup has such a huge backlog of issues.
When I started with Angular 2 there was no (usable) angular-cli. Everything changes rapidly so we have to catch up.
Do you have a working version using angular cli?
Sorry, but I don’t use angular-cli in my projects. Maybe in future when I get some time to learn about its internals and how it fits with my build-scripts.
Thanks for your work – would be nice to be able to use this as a stand alone/add on component and focus on aframe…
Wonderful article, on question: do you have this article using angular 4? I’m trying to integrate a-frame with angular 4 but I have had a lot of troubles integrating it.
Thanks in favor of sharing such a nice opinion, article is nice,
thats why i have read it completely