diff --git a/src/app/alert-form/alert-form.component.scss b/src/app/alert-form/alert-form.component.scss index fb1d601..9a1b727 100644 --- a/src/app/alert-form/alert-form.component.scss +++ b/src/app/alert-form/alert-form.component.scss @@ -1,72 +1,3 @@ -.form-field-group { - display: flex; - gap: 1rem; - flex-wrap: wrap; -} -.form-field { - display: flex; - flex-direction: column; - margin-bottom: 1rem; - flex-grow: 1; -} - -input, -textarea, -select, -ul.list-box { - background: color-mix(in srgb, var(--pill-accent) 10%, transparent); - font-size: 1rem; - padding: 0.75rem 0.5rem; - border: 2px solid color-mix(in srgb, var(--electric-violet) 30%, transparent); - border-radius: 0.5rem; - font-family: var(--font-family); - - &::placeholder { - color: var(--grayr-400); - } -} - -input, -textarea, -select, -button, ul.list-box li { - &:focus-within { - outline-width: 3px; - outline-style: solid; - outline-color: lightblue; - outline-offset: 2px; - } -} - -label:has(+ input, + textarea, + select, +ul.list-box) { - color: var(--electric-violet); - font-weight: bold; - font-size: 0.875rem; - margin-bottom: 0.2rem; - margin-left: 0.5rem; -} - -label:has(+ input[required])::after, -label:has(+ textarea[required])::after, -label:has(+ select[required])::after { - content: " *"; -} -label:has(+ input[required][aria-invalid="true"])::after, -label:has(+ textarea[required][aria-invalid="true"])::after, -label:has(+ select[required][aria-invalid="true"])::after { - color: var(--hot-red); -} - -p.description { - color: var(--gray-900); - font-size: 0.75rem; - margin-top: 0.2rem; - margin-left: 0.5rem; -} -input[aria-invalid="true"] + p.description { - color: #ac0000; -} - ul.list-box { display: flex; padding: 0; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 18e3b12..69114b9 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -12,6 +12,11 @@ export const routes: Routes = [ loadComponent: () => import('./deferred-loading-view/deferred-loading-view.component').then(m => m.DeferredLoadingViewComponent), title: 'Deferred Loading' }, + { + path: 'track', + loadComponent: () => import('./track-view/track-view.component').then(m => m.TrackViewComponent), + title: 'Track List Items' + }, { path: 'alerts', loadComponent: () => import('./alerts-view/alerts-view.component').then(m => m.AlertsViewComponent), diff --git a/src/app/nav/nav.component.html b/src/app/nav/nav.component.html index b0b474b..4358cdf 100644 --- a/src/app/nav/nav.component.html +++ b/src/app/nav/nav.component.html @@ -16,6 +16,14 @@ > Deferred Loading + + Track List Items + Track Demo + +

+ The example below shows the difference of tracking list options with + $index and an actual unique attribute. By tracking a unique + attribute, the object wont be completely new initialized. In fact, when the + array changes, the focus of elements will be restored/tracked correctly. +

+

+ Please enter a text in one of the input fields on the left side. After a few + seconds, items will be added to the array of the list. Keep an eye on where + you entered your text. While entering and concurrently adding new fields, your + input moves into another field as only the $index is tracked. The + list on the right tracks a unique attribute (item.id). This + ensures, the focus does not accidentally move to another input event when the + list updates concurrently. +

+

+ Every 5 seconds a new entry is inserted. +

+ +
+
+ Track: $index + @for(item of items; track $index) { +
+ + +
+ } +
+ +
+ Track: item.id + @for(item of items; track item.id) { +
+ + +
+ } +
+ +
+ Track: item + @for(item of items; track item) { +
+ + +
+ } +
+
diff --git a/src/app/track-view/track-view.component.scss b/src/app/track-view/track-view.component.scss new file mode 100644 index 0000000..2eb2dd4 --- /dev/null +++ b/src/app/track-view/track-view.component.scss @@ -0,0 +1,25 @@ +form { + margin-top: 1rem; + display: flex; + justify-content: space-between; + gap: 1rem; +} + +fieldset { + display: flex; + justify-content: space-between; + gap: .5rem; + flex-direction: column; + + legend { + font-weight: bold; + } +} + +p { + margin-bottom: .5rem; +} + +code { + color: var(--bright-blue); +} diff --git a/src/app/track-view/track-view.component.spec.ts b/src/app/track-view/track-view.component.spec.ts new file mode 100644 index 0000000..798af73 --- /dev/null +++ b/src/app/track-view/track-view.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TrackViewComponent } from './track-view.component'; + +describe('TrackViewComponent', () => { + let component: TrackViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TrackViewComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TrackViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/track-view/track-view.component.ts b/src/app/track-view/track-view.component.ts new file mode 100644 index 0000000..282bba7 --- /dev/null +++ b/src/app/track-view/track-view.component.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +interface Item { + id: number; + name: string; +} + +const fullNameItems: Item[] = [ + { id: 3, name: 'Item #3' }, + { id: 2, name: 'Item #2' }, + { id: 1, name: 'Item #1' }, +]; + +@Component({ + selector: 'app-track-view', + standalone: true, + imports: [FormsModule], + templateUrl: './track-view.component.html', + styleUrl: './track-view.component.scss' +}) +export class TrackViewComponent { + values = ['', '', '']; // Werte der Eingabefelder (initial) + items: Item[]; // Array mit Werten für *ngFor + toggle: boolean; + + constructor() { + this.items = fullNameItems; + this.toggle = false; + setInterval(() => { + if (this.items.length >= 50) { + this.items = []; + } + this.addItem(); + }, 5000); + } + + addItem() { + const num = this.items.length + 1; + this.toggle = !this.toggle; + this.items = [ + { id: num, name: `Item #${num}` }, + + // with the following line will make the third column work as referecnes of the objects are kept + // ...this.items, + + // with the follwoing line will make the third column not working as the references are not the same anymore (item === item => false) + ...structuredClone(this.items), + ]; + } +} diff --git a/src/styles.scss b/src/styles.scss index fe5d8ef..468e757 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -95,6 +95,75 @@ main { } } +.form-field-group { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} +.form-field { + display: flex; + flex-direction: column; + margin-bottom: 1rem; + flex-grow: 1; +} + +input, +textarea, +select, +ul.list-box { + background: color-mix(in srgb, var(--pill-accent) 10%, transparent); + font-size: 1rem; + padding: 0.75rem 0.5rem; + border: 2px solid color-mix(in srgb, var(--electric-violet) 30%, transparent); + border-radius: 0.5rem; + font-family: var(--font-family); + + &::placeholder { + color: var(--grayr-400); + } +} + +input, +textarea, +select, +button, ul.list-box li { + &:focus-within { + outline-width: 3px; + outline-style: solid; + outline-color: lightblue; + outline-offset: 2px; + } +} + +label:has(+ input, + textarea, + select, +ul.list-box) { + color: var(--electric-violet); + font-weight: bold; + font-size: 0.875rem; + margin-bottom: 0.2rem; + margin-left: 0.5rem; +} + +label:has(+ input[required])::after, +label:has(+ textarea[required])::after, +label:has(+ select[required])::after { + content: " *"; +} +label:has(+ input[required][aria-invalid="true"])::after, +label:has(+ textarea[required][aria-invalid="true"])::after, +label:has(+ select[required][aria-invalid="true"])::after { + color: var(--hot-red); +} + +p.description { + color: var(--gray-900); + font-size: 0.75rem; + margin-top: 0.2rem; + margin-left: 0.5rem; +} +input[aria-invalid="true"] + p.description { + color: #ac0000; +} + .alerts-overlay { right: 0;