Custom Abstractions
Most of angular-three-soba are custom abstractions built on top of angular-three.
Common use-cases for building custom abstractions are:
- Reuse functionalities / behaviors like an orbit control
NgtsOrbitControlsor a text componentNgtsText - Wrap a 3rd-party THREE.js object to provide Angular declarative APIs like a globe
Building a custom abstraction is similar to buildign a custom component or directive. However, there are a couple of things that you might want to look out for.
More than often, a custom abstraction would wrap a THREE.js object like a Group or Mesh and you usually want the consumers to be able to use your abstraction
like they use THREE.js object being wrapped.
In other words, the consumers should be able to pass position, rotation, scale to the abstraction; or the consumers should be able to render children for the abstraction; or the consumers don’t have to think about extend() anything to use the abstraction. Things should just work as long as they import it and drop it on the template.
Extend the catalogue
The abstraction should extend() what it actually uses. The best place to do this is in the constructor
import { extend } from 'angular-three';import { Group } from 'three';
@Component({})export class Billboard { constructor() { extend({ Group }) }}Forward properties to wrapped THREE.js object
Angular does not have the concept of Props Spreading like other ecosystem. That said, we can accept an Object Inputs and Signals API make this a lot easier than it is before in terms of change detection.
import { NgtThreeElements, omit } from 'angular-three';import { mergeInputs } from 'ngxtension/inject-inputs';import { input } from '@angular/core';
export type BillboardOptions = Partial<NgtThreeElements['ngt-group']> & {
enabled: boolean;}
const defaultOptions: BillboardOptions = { enabled: true}
@Component({ template: ` <ngt-group [parameters]="parameters()"> </ngt-group> `})export class Billboard {
options = input(defaultOptions, { transform: mergeInputs(defaultOptions) });
parameters = omit(this.options, ['enabled']); // Signal<Partial<NgtThreeElements['ngt-group']>>
enabled = pick(this.options, 'enabled'); // Signal<boolean>}- Extend the underlying THREE.js object that you wrap allows the consumers to pass inputs into your abstraction in a type-safe way.
- Add custom properties to your abstraction if it needs it.
- Set up default options if needed
- Set up an object input,
optionsis a recommended name, with thedefaultOptionsandmergeInputs. Types will be inferred correctly. omitenabledfromoptionsso you get everything else inparameterssignal.pickenabledfromoptionsso you can have anenabledsignal. This is powerful because this is aSignal<boolean>which means it automatically has some equality check.
Content Projection
You can use regular Content Projection with ng-content. In some cases, you might require some initial setup before you can render the children. This is where ng-template is needed.
@Component({ template: ` <ngt-group> <ng-content /> </ngt-group> `})export class Billboard { template = contentChild.required(TemplateRef);}contentChild.required requires the consumers to use an ng-template as the content child of Billboard component. This allows you to have some level of enforcement when it comes to consuming this Billboard abstraction.
Make sure to check out angular-three-soba for many examples of custom abstractions over Angular Three.