diff --git a/projects/www/src/app/app.component.ts b/projects/www/src/app/app.component.ts index fdaa46210c..8b06d8224a 100644 --- a/projects/www/src/app/app.component.ts +++ b/projects/www/src/app/app.component.ts @@ -5,6 +5,7 @@ import { MenuComponent } from './components/menu.component'; import { MarkdownSymbolLinkComponent } from './components/docs/markdown-symbol-link.component'; import { AlertComponent } from './components/docs/alert.component'; import { CodeExampleComponent } from './components/docs/code-example.component'; +import { CodeTabsComponent } from './components/docs/code-tabs.component'; import { StackblitzComponent } from './components/docs/stackblitz.component'; import { FooterComponent } from './components/footer.component'; @@ -74,6 +75,11 @@ export class AppComponent { }); customElements.define('ngrx-code-example', codeExampleElement); + const codeTabsElement = createCustomElement(CodeTabsComponent, { + injector: this.injector, + }); + customElements.define('ngrx-code-tabs', codeTabsElement); + const stackblitzElement = createCustomElement(StackblitzComponent, { injector: this.injector, }); diff --git a/projects/www/src/app/components/docs/code-example.component.ts b/projects/www/src/app/components/docs/code-example.component.ts index 9d0f097b06..49345a749e 100644 --- a/projects/www/src/app/components/docs/code-example.component.ts +++ b/projects/www/src/app/components/docs/code-example.component.ts @@ -1,12 +1,34 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, ElementRef, signal, viewChild } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; @Component({ selector: 'ngrx-code-example', standalone: true, + imports: [MatIcon], template: ` + @if (header) {
{{ header }}
+ } +
- + +
+ +
`, styles: [ @@ -30,9 +52,81 @@ import { Component, Input } from '@angular/core'; padding: 0 0px; overflow-x: wrap; } + + .copy-button { + position: absolute; + top: 8px; + right: 8px; + cursor: pointer; + padding: 3px; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + } + + .copy-button.copied { + animation: copyFeedback 2s ease-in-out; + } + + @keyframes copyFeedback { + 0% { + background: rgba(207, 143, 197, 0.8); + border-color: rgba(207, 143, 197, 0.6); + color: rgba(255, 255, 255, 1); + transform: scale(1); + } + 15% { + background: rgba(207, 143, 197, 0.8); + border-color: rgba(207, 143, 197, 0.6); + color: rgba(255, 255, 255, 1); + transform: scale(1.1); + } + 30% { + background: rgba(207, 143, 197, 0.8); + border-color: rgba(207, 143, 197, 0.6); + color: rgba(255, 255, 255, 1); + transform: scale(1); + } + 80% { + background: rgba(207, 143, 197, 0.8); + border-color: rgba(207, 143, 197, 0.6); + color: rgba(255, 255, 255, 1); + transform: scale(1); + } + 100% { + background: rgba(0, 0, 0, 0.7); + border-color: rgba(255, 255, 255, 0.2); + color: rgba(255, 255, 255, 0.8); + transform: scale(1); + } + } + + .copy-button:hover { + border-color: rgba(255, 255, 255, 0.4); + color: rgba(255, 255, 255, 1); + } + + .copy-button:active { + transform: scale(0.95); + } `, ], }) export class CodeExampleComponent { @Input() header: string = ''; + codeBody = viewChild.required('codeBody'); + copied = signal(false); + + copyCode() { + if (navigator.clipboard && window.isSecureContext) { + const codeText = this.codeBody().nativeElement.textContent?.trim() || ''; + navigator.clipboard.writeText(codeText); + this.copied.set(true); + } + } + + onAnimationEnd(event: AnimationEvent) { + this.copied.set(false); + } } diff --git a/projects/www/src/app/components/docs/code-tabs.component.ts b/projects/www/src/app/components/docs/code-tabs.component.ts new file mode 100644 index 0000000000..10f7c8a359 --- /dev/null +++ b/projects/www/src/app/components/docs/code-tabs.component.ts @@ -0,0 +1,64 @@ +import { + Component, + OnInit, + ElementRef, + inject, + signal, + viewChild, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { CodeExampleComponent } from './code-example.component'; + +@Component({ + selector: 'ngrx-code-tabs', + standalone: true, + imports: [CommonModule, MatTabsModule, CodeExampleComponent], + template: ` +
+ + + @for (tab of tabs(); track tab) { + + + + } + + `, + styles: [ + ` + ngrx-code-example { + margin: 0; + } + `, + ], +}) +export class CodeTabsComponent implements OnInit { + private domSanitizer = inject(DomSanitizer); + private content = viewChild.required('content'); + protected tabs = signal([]); + + ngOnInit() { + const codeExamples = + this.content().nativeElement.querySelectorAll('ngrx-code-example') ?? []; + const examples: TabInfo[] = [...codeExamples].map((example) => + this.extractTabInfo(example) + ); + this.tabs.set(examples); + } + + private extractTabInfo(tabContent: HTMLElement): TabInfo { + return { + code: this.domSanitizer.bypassSecurityTrustHtml( + tabContent.querySelector('pre')?.parentElement?.innerHTML ?? '' + ), + header: tabContent.getAttribute('header') || '', + }; + } +} + +export interface TabInfo { + code: SafeHtml; + header: string; +} diff --git a/projects/www/src/app/components/docs/markdown-article.component.ts b/projects/www/src/app/components/docs/markdown-article.component.ts index eb3d12d2ae..1d44374197 100644 --- a/projects/www/src/app/components/docs/markdown-article.component.ts +++ b/projects/www/src/app/components/docs/markdown-article.component.ts @@ -192,7 +192,6 @@ export class MarkdownArticleComponent implements OnDestroy { } private watchHeadings() { - console.log('watchHeadings'); if (this.intersectionObserver) { this.intersectionObserver.disconnect(); this.intersectionObserver = undefined; diff --git a/projects/www/src/app/pages/guide/component-store/usage.md b/projects/www/src/app/pages/guide/component-store/usage.md index 03f13c2353..678e9ef39d 100644 --- a/projects/www/src/app/pages/guide/component-store/usage.md +++ b/projects/www/src/app/pages/guide/component-store/usage.md @@ -82,16 +82,13 @@ You can see the full example at StackBlitz: - - - - - + + + + + + + Below are the steps of integrating `ComponentStore` into a component. @@ -151,7 +148,7 @@ When it is called with a callback, the state is updated. header="src/app/slide-toggle.component.ts" path="component-store-slide-toggle/src/app/slide-toggle.component.ts" region="init"> - + ```ts constructor( private readonly componentStore: ComponentStore @@ -177,7 +174,7 @@ When a user clicks the toggle (triggering a 'change' event), instead of calling header="src/app/slide-toggle.component.ts" path="component-store-slide-toggle/src/app/slide-toggle.component.ts" region="updater"> - + ```ts @Input() set checked(value: boolean) { this.setChecked(value); @@ -210,6 +207,21 @@ Finally, the state is aggregated with selectors into two properties: This example does not have a lot of business logic, however it is still fully reactive. + + + + + + + + + ### Example 2: Service extending ComponentStore `SlideToggleComponent` is a fairly simple component and having `ComponentStore` within the component itself is still manageable. When components takes more Inputs and/or has more events within its template, it becomes larger and harder to read/maintain. @@ -239,21 +251,6 @@ You can see the examples at StackBlitz: - - - - - - - - - #### Updating the state With `ComponentStore` extracted into `PaginatorStore`, the developer is now using updaters and effects to update the state. `@Input` values are passed directly into `updater`s as their arguments. @@ -316,19 +313,19 @@ changePageSize(newPageSize: number) { `PaginatorStore` exposes the two properties: `vm$` for an aggregated _ViewModel_ to be used in the template and `page$` that would emit whenever data aggregated from a `PageEvent` changes. - - + - - + - - + + diff --git a/projects/www/src/app/pages/guide/signals/signal-state.md b/projects/www/src/app/pages/guide/signals/signal-state.md index 0c7e41ed40..11ade923fd 100644 --- a/projects/www/src/app/pages/guide/signals/signal-state.md +++ b/projects/www/src/app/pages/guide/signals/signal-state.md @@ -159,8 +159,10 @@ export class Counter { ### Example 2: SignalState in a Service - - + + + + ```ts import { inject, Injectable } from '@angular/core'; @@ -204,9 +206,9 @@ export class BookListStore { } ``` - + - + ```ts import { @@ -244,5 +246,5 @@ export class BookList { } ``` - - + + diff --git a/projects/www/src/app/pages/guide/signals/signal-store/private-store-members.md b/projects/www/src/app/pages/guide/signals/signal-store/private-store-members.md index 9c523ff8a9..e8579aed4e 100644 --- a/projects/www/src/app/pages/guide/signals/signal-store/private-store-members.md +++ b/projects/www/src/app/pages/guide/signals/signal-store/private-store-members.md @@ -3,8 +3,9 @@ SignalStore allows defining private members that cannot be accessed from outside the store by using the `_` prefix. This includes root-level state slices, properties, and methods. - - + + + ```ts import { computed } from '@angular/core'; @@ -46,9 +47,10 @@ export const CounterStore = signalStore( ); ``` - + + + - ```ts import { Component, inject, OnInit } from '@angular/core'; @@ -75,7 +77,8 @@ export class Counter implements OnInit { this.store._increment2(); // ❌ } } +} ``` - - + + \ No newline at end of file