Skip to content

Commit 37669b2

Browse files
committed
Enable dynamic themes and caching
1 parent 2e196fd commit 37669b2

File tree

7 files changed

+221
-37
lines changed

7 files changed

+221
-37
lines changed

src/app/app.component.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,23 @@ import { Component } from '@angular/core';
99
<h3 align="center">
1010
Examples of displaying local and GitHub gists and code snippets.
1111
</h3>
12-
<!-- EXAMPLE: LOCAL GIST -->
13-
<!-- EXAMPLE: FETCH GIST FROM GITHUB -->
12+
<!-- EXAMPLE: FETCH _NEW_ GIST FROM GITHUB (NOT-SAVED) -->
13+
<ngx-gist gistId="d55ea012b585a16a9970878d90106d74"></ngx-gist>
14+
<!-- EXAMPLE: FETCH _CACHED_ GIST FROM MEMORY (ON SUBSEQUENT REQUESTS) -->
1415
<ngx-gist
1516
gistId="d55ea012b585a16a9970878d90106d74"
17+
[useCache]="true"
18+
></ngx-gist>
19+
<!-- EXAMPLE: DISPLAYING A SPECIFIC FILE -->
20+
<ngx-gist
1621
displayOnlyFileName="super.js"
22+
gistId="d55ea012b585a16a9970878d90106d74"
23+
[useCache]="true"
1724
></ngx-gist>
18-
<!-- EXAMPLE: DISPLAYING SPECIFIC FILES -->
25+
<!-- TODO: SUPPORT LOCAL GIST -->
26+
<!--
27+
<ngx-gist [gist]="localGistObject"></ngx-gist>
28+
-->
1929
</ngx-body>
2030
<ngx-footer #footer></ngx-footer>
2131
`,

src/app/public/ngx-gist.component.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
@import "https://fonts.googleapis.com/icon?family=Material+Icons";
2+
3+
:host.deeppurple-amber ::ng-deep {
4+
// https://unpkg.com/@angular/material@latest/prebuilt-themes/deeppurple-amber.css
5+
// @import "https://unpkg.com/@angular/material@14.1.0/prebuilt-themes/deeppurple-amber.css";
6+
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
7+
}
8+
9+
:host.indigo-pink {
10+
// https://unpkg.com/@angular/material@latest/prebuilt-themes/indigo-pink.css
11+
@import "https://unpkg.com/@angular/material@14.1.0/prebuilt-themes/indigo-pink.css";
12+
}
13+
14+
:host.pink-bluegrey {
15+
// https://unpkg.com/@angular/material@latest/prebuilt-themes/pink-bluegrey.css
16+
@import "https://unpkg.com/@angular/material@14.1.0/prebuilt-themes/pink-bluegrey.css";
17+
}
18+
19+
:host.purble-green {
20+
// https://unpkg.com/@angular/material@latest/prebuilt-themes/purple-green.css
21+
@import "https://unpkg.com/@angular/material@14.1.0/prebuilt-themes/purple-green.css";
22+
}
23+
124
pre {
225
display: flex;
326
margin-top: 0;
Lines changed: 102 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { NgxGistService } from './ngx-gist.service';
22
import { isNonEmptyValue } from './ngx-gist.utilities';
33
import { NgxGist } from './ngx-gist.model';
4-
import { Component, Input, OnInit } from '@angular/core';
4+
import { Component, HostBinding, Inject, Input, OnInit } from '@angular/core';
55
import { Language, default as hljs } from 'highlight.js';
6-
import { BehaviorSubject, filter, firstValueFrom } from 'rxjs';
6+
import { filter, firstValueFrom, ReplaySubject } from 'rxjs';
77
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
8+
import { DOCUMENT } from '@angular/common';
89

910
@UntilDestroy()
1011
@Component({
1112
selector: 'ngx-gist',
1213
template: `
1314
<mat-card class="code-container">
15+
<!-- TODO: LOADING ICON OR MESSAGE -->
1416
<mat-tab-group *ngIf="gist as g">
1517
<mat-tab
1618
*ngFor="
@@ -27,9 +29,9 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
2729
</mat-tab-group>
2830
<mat-card-footer>
2931
<a
30-
*ngIf="gistId && gist"
32+
*ngIf="gistIdChanges | async as gid"
3133
target="_blank"
32-
[href]="'https://gist.github.com/' + gistId"
34+
[href]="'https://gist.github.com/' + gid"
3335
>
3436
<mat-icon>link</mat-icon> Open Gist on GitHub
3537
</a>
@@ -39,70 +41,140 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
3941
styleUrls: ['./ngx-gist.component.scss'],
4042
})
4143
export class NgxGistComponent implements OnInit {
42-
public constructor(public readonly ngxGistService: NgxGistService) {}
44+
public constructor(
45+
@Inject(DOCUMENT)
46+
private readonly document: Document,
47+
private readonly ngxGistService: NgxGistService,
48+
) {}
4349

4450
public codeSnippet: string | null = null;
51+
private htmlLinkElement: HTMLLinkElement | null = null;
4552

4653
/**
47-
* Cache the GitHub gist request in local memory. GitHub has a request
48-
* limit so using this is wise in making sure users always get the content,
49-
* especially on quick refreshes or having multiple gist snippets on page.
50-
*/
51-
@Input() public cacheForMin?: number;
52-
53-
/**
54-
* Display in the DOM only the selected filename.
54+
* Display in the DOM only the selected filename from the gists files array.
55+
*
56+
* TODO: Make this possible for string array input.
57+
*
58+
* Default: `undefined`
5559
*/
5660
@Input() public displayOnlyFileName?: string;
57-
5861
/**
5962
* Provide a static gist model here directly which will be displayed if
6063
* no `gistId` is provided for remote fetching. Also this model will be
6164
* displayed should a fetch fail when retrieving `gistId`, or overwritten
6265
* once the pertaining `gistId` data is fetched.
66+
*
67+
* Default: `undefined`
6368
*/
6469
@Input() public gist?: NgxGist;
65-
6670
// We want reactive behavior for `gistId` so we can update gists asynchronously
67-
private readonly gistIdSubject = new BehaviorSubject<
71+
private readonly gistIdSubject = new ReplaySubject<
6872
NgxGistComponent['gistId']
69-
>(null);
70-
73+
>(1);
74+
public readonly gistIdChanges = this.gistIdSubject.asObservable();
7175
/**
7276
* Provide the GitHub gist id to be fetched and loaded. This can be found in
7377
* URL of the gists you create. For example the id `TH1515th31DT0C0PY` in:
7478
* https://gist.github.com/FakeUserName/TH1515th31DT0C0PY
7579
*
7680
* Alternatively, provide a value directly in the sibling input `gist`.
7781
*/
78-
@Input() public set gistId(value: string | null) {
82+
@Input() public set gistId(value: string) {
7983
this.gistIdSubject.next(value);
8084
}
81-
85+
/**
86+
* When defined, override automatic language detection [and styling] and
87+
* treat all gists as this lanuage.
88+
*
89+
* See supported languages here:
90+
* https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md
91+
*
92+
* Default: `undefined`
93+
*/
8294
@Input() public languageName?: Language['name'];
95+
/**
96+
* Define a material core theme to apply. Ideally, you should already have
97+
* your global material theme set at the root of your project so try to
98+
* avoid using this if possible. Note: These are also loaded from a CDN.
99+
*
100+
* See theming Angular Material: https://material.angular.io/guide/theming
101+
*
102+
* CDN used: `https://unpkg.com`
103+
*
104+
* Default: `undefined`
105+
*/
106+
@Input() @HostBinding('class') public materialTheme:
107+
| 'deeppurple-amber'
108+
| 'indigo-pink'
109+
| 'pink-bluegrey'
110+
| 'purple-green'
111+
| undefined = undefined;
112+
/**
113+
* Cache the GitHub gist request in local memory for 24 hours. GitHub has a
114+
* request limit, so this helps in reducing bandwidth. Loads previously
115+
* fetched gist content from the users machine.
116+
*
117+
* Default: `true`
118+
*/
119+
@Input() public useCache = true;
83120

84121
public async ngOnInit(): Promise<void> {
85-
this.gistIdSubject
122+
this.setTheme();
123+
124+
this.gistIdChanges
86125
.pipe(filter(isNonEmptyValue), untilDestroyed(this))
87126
.subscribe(async (gistId) => {
88-
// Use the initial gist model as a fallback for a failed fetch. This
89-
// enables us to have a fallback gist snippet should we be offline or
90-
// the data is unavailable for some reason.
91-
const initialGist = this.gist ? { ...this.gist } : undefined;
92-
// Fetch and hydrate model or fallback to initial gist.
93-
this.gist =
94-
(await firstValueFrom(this.ngxGistService.get(gistId))) ??
95-
initialGist;
127+
if (this.useCache) {
128+
const cachedValue = this.ngxGistService.getFromCache(gistId);
129+
if (cachedValue) {
130+
// Value is cached and not previously expired, use it.
131+
this.gist = cachedValue;
132+
return;
133+
}
134+
}
135+
136+
await this.fetchAndSetGist(gistId);
96137
});
97138
}
98139

140+
// TODO: Work on speeding this call up. Or possibly pre-render instead.
99141
public getHighlightJsContent(value: string): string {
100142
const userSpecifiedLanguage = this.languageName;
101-
102143
if (userSpecifiedLanguage) {
103144
return hljs.highlight(value, { language: userSpecifiedLanguage }).value;
104145
}
105146

106147
return hljs.highlightAuto(value).value;
107148
}
149+
150+
private async fetchAndSetGist(gistId: string): Promise<void> {
151+
// Use the initial gist model as a fallback for a failed fetch. This
152+
// enables us to have a fallback gist snippet should we be offline or
153+
// the data is unavailable for some reason.
154+
const initialGist = this.gist ? { ...this.gist } : undefined;
155+
156+
// Fetch and hydrate model or fallback to initial gist.
157+
this.gist =
158+
(await firstValueFrom(this.ngxGistService.get(gistId))) ?? initialGist;
159+
160+
if (this.useCache && this.gist) {
161+
// Set value in cache for reuse saving on the amount of HTTP requests.
162+
// Set refresh time to be a hard coded 24 hours. This was once configurable
163+
// but I decided against it for simplicities sake on ease of use.
164+
this.ngxGistService.setToCache(this.gist, 1440);
165+
}
166+
}
167+
168+
private setTheme(): void {
169+
if (!this.materialTheme) {
170+
return;
171+
}
172+
173+
this.htmlLinkElement = this.document.createElement('link');
174+
this.htmlLinkElement.href = `https://unpkg.com/@angular/material@14.1.0/prebuilt-themes/${this.materialTheme}.css`;
175+
this.htmlLinkElement.media = 'screen,print';
176+
this.htmlLinkElement.rel = 'stylesheet';
177+
this.htmlLinkElement.type = 'text/css';
178+
this.document.head.appendChild(this.htmlLinkElement);
179+
}
108180
}

src/app/public/ngx-gist.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ const gistFilesCodec = io.readonly(
171171
'GistFiles',
172172
);
173173

174-
const gistCodec = io.readonly(
174+
export const gistCodec = io.readonly(
175175
io.intersection([
176176
io.type({
177177
comments: io.number,

src/app/public/ngx-gist.service.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
import { NgxGist } from './ngx-gist.model';
1+
import { gistCodec, NgxGist } from './ngx-gist.model';
22
import { Injectable } from '@angular/core';
33
import { HttpClient } from '@angular/common/http';
44
import { catchError, map, Observable, of } from 'rxjs';
5+
import {
6+
decodeValueElseNull,
7+
isNonEmptyString,
8+
parsedJsonFromStringCodec,
9+
} from './ngx-gist.utilities';
10+
import * as io from 'io-ts';
511

612
@Injectable()
713
export class NgxGistService {
814
public constructor(private readonly httpClient: HttpClient) {}
915

16+
private readonly delimiter = '||';
17+
1018
/**
1119
* Fetch gist data from GitHub.
1220
*
@@ -26,4 +34,64 @@ export class NgxGistService {
2634
map((response) => NgxGist.deserialize(response) ?? null),
2735
);
2836
}
37+
38+
public getFromCache(gistId: string): NgxGist | null {
39+
const key = `gist${this.delimiter + gistId}`;
40+
const value = localStorage.getItem(key);
41+
if (value === null || !isNonEmptyString(value)) {
42+
// Doesn't exist in memory
43+
return null;
44+
}
45+
46+
const storedGist = decodeValueElseNull(storedGistFromJsonStringCodec)(
47+
value,
48+
);
49+
if (!storedGist) {
50+
// Failed to deserialize stored data (corruption?). Remote it and return.
51+
localStorage.removeItem(key);
52+
return null;
53+
}
54+
55+
const now = new Date();
56+
if (now.getTime() > storedGist.expiration) {
57+
// Stored value has since expired, remove it and return.
58+
localStorage.removeItem(key);
59+
return null;
60+
}
61+
62+
const gist = storedGist.value;
63+
// All is good, return unexpired gist
64+
return {
65+
...gist,
66+
created_at: new Date(gist.created_at),
67+
updated_at: new Date(gist.updated_at),
68+
};
69+
}
70+
71+
public setToCache(gist: NgxGist, expiresInMin?: number): void {
72+
const key = `gist${this.delimiter + gist.id}`;
73+
const now = new Date();
74+
const expiresInMs = expiresInMin ? expiresInMin * 60000 : 0;
75+
const value: StoredGist = {
76+
expiration: now.getTime() + expiresInMs,
77+
value: gist,
78+
};
79+
const stringValue = JSON.stringify(value);
80+
localStorage.setItem(key, stringValue);
81+
}
2982
}
83+
84+
const storedGistCodec = io.readonly(
85+
io.type({
86+
expiration: io.number,
87+
value: gistCodec,
88+
}),
89+
'StoredGist',
90+
);
91+
92+
const storedGistFromJsonStringCodec = parsedJsonFromStringCodec.pipe(
93+
storedGistCodec,
94+
'StoredGistFromJsonString',
95+
);
96+
97+
type StoredGist = io.TypeOf<typeof storedGistCodec>;

src/app/public/public.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/** Public API Exports for Node Package */
22

3-
export * from './ngx-gist.component';
43
export * from './ngx-gist-content.pipe';
4+
export * from './ngx-gist-file-filter.pipe';
5+
export * from './ngx-gist.component';
56
export * from './ngx-gist.model';
67
export * from './ngx-gist.module';
7-
export * from './ngx-gist-file-filter.pipe';
88
export * from './ngx-gist.service';
99
export * from './ngx-gist.utilities';

src/styles.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
/* You can add global styles to this file, and also import other style files */
2+
3+
// It's wise to import a prebuilt Material theme or custom theme at the root of
4+
// your project when using `@proangular/ngx-gist`.
5+
6+
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
7+
8+
/* Alternative pre-built themes.
29
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
10+
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
11+
@import "~@angular/material/prebuilt-themes/pink-bluegrey.css";
12+
@import "~@angular/material/prebuilt-themes/purple-green.css";
13+
*/
314

415
body {
516
padding: 0;

0 commit comments

Comments
 (0)