Skip to content

Commit 8201bbb

Browse files
committed
feat(sanity): sanity image directive
1 parent fb3b628 commit 8201bbb

14 files changed

Lines changed: 200 additions & 101 deletions

File tree

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,35 @@
1-
import { ChangeDetectionStrategy, Component } from '@angular/core';
2-
import { NgOptimizedImage } from '@angular/common';
1+
import { ChangeDetectionStrategy, Component, computed } from '@angular/core';
32

4-
import { provideSanityLoader } from '@limitless-angular/sanity/image-loader';
5-
import { PortableTextTypeComponent } from '@limitless-angular/sanity/portabletext';
6-
7-
interface Asset {
8-
_ref: string;
9-
_type: string;
10-
}
3+
import { getImageDimensions } from '@sanity/asset-utils';
114

12-
export interface ImageBlock {
13-
_type: 'image';
14-
_key: string;
15-
asset: Asset;
16-
}
5+
import {
6+
provideSanityLoader,
7+
SanityImage,
8+
} from '@limitless-angular/sanity/image-loader';
9+
import { PortableTextTypeComponent } from '@limitless-angular/sanity/portabletext';
1710

1811
@Component({
1912
selector: 'app-image',
2013
standalone: true,
21-
imports: [NgOptimizedImage],
14+
imports: [SanityImage],
2215
template: `
2316
<img
2417
class="mx-auto"
25-
[ngSrc]="value().asset._ref"
26-
[width]="300"
27-
[height]="300"
2818
alt="Sanity Image"
19+
[sanityImage]="value()"
20+
[width]="dimensions().width"
21+
[height]="dimensions().height"
2922
/>
3023
`,
3124
styles: `
3225
:host {
33-
@apply block h-[300px];
26+
@apply block;
3427
}
3528
`,
3629
changeDetection: ChangeDetectionStrategy.OnPush,
3730
// Project data taken from https://www.sanity.io/demos/studio
3831
providers: [provideSanityLoader({ projectId: 'k4hg38xw', dataset: 'demo' })],
3932
})
40-
export class ImageComponent extends PortableTextTypeComponent<ImageBlock> {}
33+
export class ImageComponent extends PortableTextTypeComponent {
34+
dimensions = computed(() => getImageDimensions(this.value()));
35+
}

apps/sanity-example/src/app/fixture.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { PortableTextBlock } from '@portabletext/types';
22
import { AnnotatedMapBlock } from './components/annotated-map.component';
3-
import { ImageBlock } from './components/image.component';
3+
4+
interface ImageBlock {
5+
_type: 'image';
6+
[key: string]: any;
7+
}
48

59
interface CodeBlock {
610
_type: 'code';
@@ -34,7 +38,12 @@ interface CodeBlock {
3438
export class CodeComponent extends PortableTextTypeComponent<CodeBlock> {}
3539
`.trim();
3640

37-
export const blocks: (PortableTextBlock | CodeBlock | AnnotatedMapBlock | ImageBlock)[] = [
41+
export const blocks: (
42+
| PortableTextBlock
43+
| CodeBlock
44+
| AnnotatedMapBlock
45+
| ImageBlock
46+
)[] = [
3847
{
3948
_type: 'block',
4049
_key: 'head',
@@ -551,7 +560,7 @@ export const blocks: (PortableTextBlock | CodeBlock | AnnotatedMapBlock | ImageB
551560
_type: 'block',
552561
_key: 'image-loader-header',
553562
style: 'h2',
554-
children: [{ _type: 'span', _key: 'a', text: 'Sanity Image Loader' }],
563+
children: [{ _type: 'span', _key: 'a', text: 'Sanity Image Awesomeness' }],
555564
},
556565
{
557566
_type: 'block',
@@ -561,7 +570,7 @@ export const blocks: (PortableTextBlock | CodeBlock | AnnotatedMapBlock | ImageB
561570
children: [
562571
{
563572
_type: 'span',
564-
text: "Alright, let's talk about images for a sec. You know how sometimes images can be a pain to deal with in web apps? Well, we've got something pretty cool for that.",
573+
text: "Alright, let's talk about some cool image stuff for your Angular apps with Sanity. We've cooked up not one, but two nifty features to make your life easier.",
565574
},
566575
],
567576
},
@@ -573,7 +582,7 @@ export const blocks: (PortableTextBlock | CodeBlock | AnnotatedMapBlock | ImageB
573582
children: [
574583
{
575584
_type: 'span',
576-
text: "We've cooked up this thing called the Image Loader Provider. It's basically a neat way to make Sanity and Angular play nice together when it comes to loading images. You get all the cool stuff Sanity can do with images, but it works smoothly with Angular's fancy NgOptimizedImage directive.",
585+
text: "First up, we've got this sweet little thing called the Sanity Image Directive. It's like a magic wand for your images. You just slap a sanityImage attribute on your img tag, and bam! Your Sanity images are right there, looking all pretty and optimized. It's like telling your images, \"Hey, you're a Sanity image now. Act cool.\"\n",
577586
},
578587
],
579588
},
@@ -585,7 +594,19 @@ export const blocks: (PortableTextBlock | CodeBlock | AnnotatedMapBlock | ImageB
585594
children: [
586595
{
587596
_type: 'span',
588-
text: 'Check this example:',
597+
text: "Now, let's chat about its sidekick - the Image Loader Provider. This is the behind-the-scenes hero that makes sure everything runs smooth as butter. It's basically a neat way to make Sanity and Angular play nice together when it comes to loading images. You get all the cool stuff Sanity can do with images, but it works smoothly with Angular's fancy NgOptimizedImage directive.",
598+
},
599+
],
600+
},
601+
{
602+
_type: 'block',
603+
_key: 'image-loader-intrp-4',
604+
style: 'normal',
605+
markDefs: [],
606+
children: [
607+
{
608+
_type: 'span',
609+
text: 'Together, these two are like having a personal assistant for your images. You just point and say, "I want that image there," and our tools make sure it happens, all optimized and fancy-like. Pretty cool, right?',
589610
},
590611
],
591612
},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { provideSanityLoader } from './loader';
2+
export { SanityImage } from './sanity-image.directive';
Lines changed: 49 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,59 @@
11
import { Provider } from '@angular/core';
22
import { IMAGE_LOADER, ImageLoaderConfig } from '@angular/common';
3+
34
import imageUrlBuilder from '@sanity/image-url';
4-
import type {
5-
SanityImageSource,
6-
SanityImageObject,
7-
SanityReference,
8-
SanityAsset,
9-
} from '@sanity/image-url/lib/types/types';
105

11-
interface SanityConfig {
12-
projectId: string;
13-
dataset: string;
14-
}
6+
import { SANITY_CONFIG, SanityConfig } from '@limitless-angular/sanity/shared';
157

168
const DEFAULT_IMAGE_QUALITY = 75;
179

18-
function getSanityRefId(image: SanityImageSource): string {
19-
if (typeof image === 'string') {
20-
return image;
21-
}
22-
23-
const obj = image as SanityImageObject;
24-
const ref = image as SanityReference;
25-
const img = image as SanityAsset;
26-
27-
if (obj.asset) {
28-
return obj.asset._ref || (obj.asset as SanityAsset)._id;
29-
}
30-
31-
return ref._ref || img._id || '';
32-
}
33-
3410
export function provideSanityLoader(config: SanityConfig): Provider {
35-
return {
36-
provide: IMAGE_LOADER,
37-
useValue: (loaderConfig: ImageLoaderConfig) => {
38-
const {
39-
src,
40-
loaderParams = {},
41-
width = loaderParams['width'],
42-
isPlaceholder,
43-
} = loaderConfig;
44-
const image = src as SanityImageSource;
45-
const id = getSanityRefId(image);
46-
if (!id) {
47-
throw new Error('Invalid Sanity image source');
48-
}
49-
50-
const builder = imageUrlBuilder(config);
51-
let imageBuilder = builder
52-
.image(image)
53-
.auto('format')
54-
.fit((loaderParams['fit'] ?? loaderParams['height']) ? 'min' : 'max');
55-
56-
if (width && loaderParams['height'] && loaderParams['width']) {
57-
imageBuilder = imageBuilder.height(
58-
Math.round((loaderParams['height'] / loaderParams['width']) * width),
59-
);
60-
}
61-
62-
if (width) {
63-
imageBuilder = imageBuilder.width(width);
64-
}
65-
66-
// Use loaderParams for additional configuration
67-
if (loaderParams['quality']) {
68-
imageBuilder = imageBuilder.quality(loaderParams['quality']);
69-
} else {
70-
imageBuilder = imageBuilder.quality(DEFAULT_IMAGE_QUALITY);
71-
}
72-
73-
if (loaderParams['blur']) {
74-
imageBuilder = imageBuilder.blur(loaderParams['blur']);
75-
}
76-
77-
if (isPlaceholder) {
78-
imageBuilder = imageBuilder.blur(50).quality(20);
79-
}
80-
81-
return imageBuilder.url() || '';
11+
return [
12+
{ provide: SANITY_CONFIG, useValue: config },
13+
{
14+
provide: IMAGE_LOADER,
15+
useValue: (loaderConfig: ImageLoaderConfig) => {
16+
const {
17+
src,
18+
loaderParams = {},
19+
width = loaderParams['width'],
20+
isPlaceholder,
21+
} = loaderConfig;
22+
const builder = imageUrlBuilder(config);
23+
let imageBuilder = builder
24+
.image(src)
25+
.auto('format')
26+
.fit((loaderParams['fit'] ?? loaderParams['height']) ? 'min' : 'max');
27+
28+
if (width && loaderParams['height'] && loaderParams['width']) {
29+
imageBuilder = imageBuilder.height(
30+
Math.round(
31+
(loaderParams['height'] / loaderParams['width']) * width,
32+
),
33+
);
34+
}
35+
36+
if (width) {
37+
imageBuilder = imageBuilder.width(width);
38+
}
39+
40+
// Use loaderParams for additional configuration
41+
if (loaderParams['quality']) {
42+
imageBuilder = imageBuilder.quality(loaderParams['quality']);
43+
} else {
44+
imageBuilder = imageBuilder.quality(DEFAULT_IMAGE_QUALITY);
45+
}
46+
47+
if (loaderParams['blur']) {
48+
imageBuilder = imageBuilder.blur(loaderParams['blur']);
49+
}
50+
51+
if (isPlaceholder) {
52+
imageBuilder = imageBuilder.blur(50).quality(20);
53+
}
54+
55+
return imageBuilder.url() || '';
56+
},
8257
},
83-
};
58+
];
8459
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
computed,
3+
Directive,
4+
inject,
5+
Input,
6+
input,
7+
OnInit,
8+
} from '@angular/core';
9+
import { NgOptimizedImage } from '@angular/common';
10+
11+
import type { SanityImageSource } from '@sanity/image-url/lib/types/types';
12+
import imageUrlBuilder from '@sanity/image-url';
13+
14+
import { SANITY_CONFIG } from '@limitless-angular/sanity/shared';
15+
16+
@Directive({
17+
// eslint-disable-next-line @angular-eslint/directive-selector
18+
selector: 'img[sanityImage]',
19+
standalone: true,
20+
})
21+
// eslint-disable-next-line @angular-eslint/directive-class-suffix
22+
export class SanityImage extends NgOptimizedImage implements OnInit {
23+
private _loaderParams!: Record<string, any>;
24+
25+
sanityImage = input.required<SanityImageSource>();
26+
27+
@Input()
28+
// @ts-expect-error we want to add some internal properties to loaderParams input
29+
override set loaderParams(loaderParams: Record<string, any>) {
30+
this._loaderParams = this.prepareLoaderParams(loaderParams);
31+
}
32+
33+
override get loaderParams(): Record<string, any> {
34+
return this._loaderParams;
35+
}
36+
37+
@Input() override ngSrc!: string;
38+
39+
quality = input<number>();
40+
41+
private computedSrc = computed(() => this.constructSanityUrl());
42+
43+
private sanityConfig = inject(SANITY_CONFIG);
44+
45+
override ngOnInit() {
46+
this.ngSrc = this.computedSrc();
47+
if (!this._loaderParams) {
48+
this._loaderParams = this.prepareLoaderParams({});
49+
}
50+
51+
super.ngOnInit();
52+
}
53+
54+
private constructSanityUrl() {
55+
return imageUrlBuilder(this.sanityConfig).image(this.sanityImage()).url();
56+
}
57+
58+
private prepareLoaderParams(
59+
loaderParams: Record<string, any>,
60+
): Record<string, any> {
61+
const params: Record<string, any> = {};
62+
if (this.width) {
63+
params['width'] = this.width;
64+
}
65+
66+
if (this.height) {
67+
params['height'] = this.height;
68+
}
69+
70+
if (this.quality()) {
71+
params['quality'] = this.quality();
72+
}
73+
74+
return { ...params, ...loaderParams };
75+
}
76+
}

libs/sanity/shared/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# @limitless-angular/sanity/shared
2+
3+
Secondary entry point of `@limitless-angular/sanity`. It can be used by importing from `@limitless-angular/sanity/shared`.

libs/sanity/shared/ng-package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lib": {
3+
"entryFile": "src/index.ts"
4+
}
5+
}

libs/sanity/shared/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './tokens';
2+
export * from './types';

libs/sanity/shared/src/tokens.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { InjectionToken } from '@angular/core';
2+
3+
import { SanityConfig } from './types';
4+
5+
export const SANITY_CONFIG = new InjectionToken<SanityConfig>('SANITY_CONFIG');

libs/sanity/shared/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface SanityConfig {
2+
projectId: string;
3+
dataset: string;
4+
}

0 commit comments

Comments
 (0)