Layout Projection and declarative layout animation for Angular
npm i @layout-projection/{core,animation,angular}
The Angular adapter depends on several fundamental services to function. Use provideLayoutProjectionBuiltinSetup
to provide the pre-built implementations of these services:
import { provideLayoutProjectionBuiltinSetup } from '@layout-projection/angular/setup';
export const APP_CONFIG: ApplicationConfig = {
providers: [provideLayoutProjectionBuiltinSetup()],
};
The Angular adapter introduces two handy directives, LayoutNode
and LayoutNodeAnimator
, that allows you to animate layout updates simply by adding an attribute:
import { LayoutNode, LayoutNodeAnimator } from '@layout-projection/angular';
@Component({
imports: [LayoutNode, LayoutNodeAnimator]
})
<div layout></div>
This layout
attribute animates any layout changes, including previously unavailable CSS values such as switching justify-content
between flex-start
and flex-end
:
<div
layout
[style.justify-content]="active ? 'flex-start' : 'flex-end'"
></div>
It is not required to change the layout via inline styles. You can change the layout by whatever means you like, like switching the presence of a CSS class or updating a custom attribute.
The layout change should happen immediately - no CSS transition or animation should be applied. The directives will take care of the animation and smoothly transition the layout from the old state to the new state.
A layout change can be anything:
- updating
width
orheight
- changing number of grid columns
- adding or removing items from a list
- reordering the existing items in a list
Sometimes one visual element might be represented by different DOM elements in different states. For example, an underline in a tabs component might be different DOM elements when switching between tabs.
Such transitions can be implemented by assigning the same layout ID to the elements that represent the same visual element, which will result in a shared element transition.
The layout ID can be assigned by providing a value to the layout
attribute:
<!-- prettier-ignore -->
```html
@for (tab of tabs; track $index) {
<div class="tab" (click)="selectedTab = tab">
<div class="tab-content">{{ tab.label }}</div>
@if (tab === selectedTab) {
<div class="tab-underline" layout="tab-underline"></div>
}
</div>
}
Formally, in order to initiate a shared element transition:
- the old element must be removed before the new element is inserted, and
- the new element must be inserted immediately after the old element is removed (within one event loop), and
- the old and new elements must have the same layout ID
The layout ID can be dynamically determined by providing a binding expression to the layout
attribute:
<div class="tab" [layout]="tab.id"></div>
The expression bound to the layout
attribute should be stable. If the expression yields multiple values, only the first value will be used as the layout ID.
If an element is assigned an HTML id
, the HTML id
will be used as the layout ID.
The following two snippets are equivalent:
<div class="tab-underline" layout="tab-underline"></div>
<div id="tab-underline" layout></div>
If both the id
attribute and the layout
attribute are assigned a value, the
layout
attribute will take precedence.
<div id="tab-underline" layout="underline"></div>
In the above example, the layout ID is underline
.
The layout animations are driven by CSS transform
styles, in order to take advantage of GPU acceleration and achieve high performance.
However, the transform
styles can sometimes visually distort children. This is especially noticeable when the layout change involves size updates, where scaling needs to be applied to animate the size change.
<div class="parent" [class.open]="open" layout (click)="open = !open">
<div class="child"></div>
</div>
To fix this, you can apply the directives on the direct children to cancel out the distortion:
<div class="parent" [class.open]="open" layout (click)="open = !open">
<div class="child" layout></div>
</div>
Transforms can also distort box-shadow
and border-radius
styles. The pre-built setup has already included the distortion cancellation for these two styles.
Some elements change their visual appearance between different aspect ratios, such as images and text. In such cases, you might want to skip the size animation and only animate the position changes.
The pre-built setup considers two metadata attributes during animations:
-
SKIP_SIZE
skips the size animation -
SKIP_POSITION
skips the position animation
These metadata can be defined via the corresponding directives.
In order to skip the size animation for a specific element:
import { LayoutNode, LayoutNodeAnimator, SkipSize } from '@layout-projection/angular';
@Component({
imports: [LayoutNode, LayoutNodeAnimator, SkipSize]
})
<p layout skipSize>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
A setup refers to a collection of fundamental services provided at the root injector that is used by the directives to perform animations.
While the pre-built setup should be sufficient for most use cases, you can customize the setup by providing your own implementations of the services.
All the Layout Projection packages are designed following the SOLID principles and are well documented with JSDoc, making it relatively easy to hook into any procedure with your customized code. See the source code for more information.