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
NgtsOrbitControls
or 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,
options
is a recommended name, with thedefaultOptions
andmergeInputs
. Types will be inferred correctly. omit
enabled
fromoptions
so you get everything else inparameters
signal.pick
enabled
fromoptions
so you can have anenabled
signal. 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.