new song GUI

This commit is contained in:
Benjamin Ifland
2019-03-25 15:13:52 +01:00
parent 881729ed84
commit dc01c60e67
24 changed files with 293 additions and 138 deletions

View File

@@ -21,9 +21,11 @@ import { TableComponent } from './components/songs/table/table.component';
import { SongsComponent } from './components/songs/songs.component';
import { SongComponent } from './components/songs/song/song.component';
import { SongEditComponent } from './components/songs/song-edit/song-edit.component';
import { SongNewComponent } from './components/songs/song-new/song-new.component';
import { SongFormComponent } from './components/songs/song-form/song-form.component';
@NgModule({
declarations: [AppComponent, SongsComponent, TableComponent, SongComponent, SongEditComponent],
declarations: [AppComponent, SongsComponent, TableComponent, SongComponent, SongEditComponent, SongNewComponent, SongFormComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,

View File

@@ -1,61 +1,18 @@
<div class="song-detail-container">
<mat-card class="mat-elevation-z8" [@blend] *ngIf="form">
<mat-card class="mat-elevation-z8" *ngIf="form">
<mat-card-header>
<div mat-card-avatar>
<button mat-icon-button (click)="onBack()">
<button mat-icon-button (click)="onBack()" color="warn">
<fa-icon [icon]="faArrow"></fa-icon>
</button>
</div>
<mat-card-title>Titel bearbeiten</mat-card-title>
<mat-card-subtitle>Daten werden nach der Eingabe automatisch gespeichert</mat-card-subtitle>
<mat-card-subtitle>
Daten werden nach der Eingabe automatisch gespeichert
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form>
<mat-form-field>
<input
matInput
placeholder="Titel"
[formControl]="form.controls.Name"
/>
</mat-form-field>
<div class="row">
<mat-radio-group [formControl]="form.controls.SongType">
<mat-radio-button value="Praise">Lobpreis</mat-radio-button>
<mat-radio-button value="Worship">Anbetung</mat-radio-button>
</mat-radio-group>
<mat-form-field>
<mat-label>Tonart</mat-label>
<mat-select [formControl]="form.controls.Key">
<mat-option *ngFor="let key of keys" [value]="key">
{{ key }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="Tempo"
[formControl]="form.controls.Tempo"
/>
</mat-form-field>
</div>
<mat-form-field>
<textarea
matInput
placeholder="Liedtext"
[formControl]="form.controls.Text"
[matTextareaAutosize]="true"
></textarea>
</mat-form-field>
<mat-form-field>
<textarea
matInput
placeholder="Kommentare"
[formControl]="form.controls.Comments"
[matTextareaAutosize]="true"
></textarea>
</mat-form-field>
</form>
<app-song-form [form]="form"></app-song-form>
</mat-card-content>
<!-- <mat-card-actions>
<button mat-button (click)="onClickDownload()">Herunterladen</button>

View File

@@ -1,9 +1,3 @@
form {
display: flex;
flex-direction: column;
.row {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
grid-column-gap: 20px;
:host {
display: block;
}
}

View File

@@ -6,56 +6,20 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { blend } from 'src/app/services/animation';
import { EditSongService } from 'src/app/data/edit-song.service';
import { faLongArrowAltLeft } from '@fortawesome/free-solid-svg-icons';
import { State } from 'src/app/data/state';
@Component({
selector: 'app-song-edit',
templateUrl: './song-edit.component.html',
styleUrls: ['./song-edit.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [blend]
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SongEditComponent implements OnInit {
public form: FormGroup = null;
public faArrow = faLongArrowAltLeft;
public keys = [
'C',
'C#',
'Db',
'D',
'D#',
'Eb',
'E',
'F',
'F#',
'Gb',
'G',
'G#',
'Ab',
'A',
'A#',
'B',
'H',
'c',
'c#',
'db',
'd',
'd#',
'eb',
'e',
'f',
'f#',
'gb',
'g',
'g#',
'ab',
'a',
'A#',
'b',
'h'
];
constructor(
private editSongService: EditSongService,
@@ -64,11 +28,11 @@ export class SongEditComponent implements OnInit {
) {}
ngOnInit() {
this.form = this.editSongService.initEditForm();
this.form = this.editSongService.initEditForm(true);
this.change.markForCheck();
}
public onBack(): void {
this.songsService.edit = false;
this.songsService.state = State.read;
}
}

View File

@@ -0,0 +1,36 @@
export const keys = [
'C',
'C#',
'Db',
'D',
'D#',
'Eb',
'E',
'F',
'F#',
'Gb',
'G',
'G#',
'Ab',
'A',
'A#',
'B',
'H',
'c',
'c#',
'db',
'd',
'd#',
'eb',
'e',
'f',
'f#',
'gb',
'g',
'g#',
'ab',
'a',
'A#',
'b',
'h'
];

View File

@@ -0,0 +1,53 @@
<form>
<mat-form-field>
<input
matInput
placeholder="Titel"
[formControl]="form.controls.Name"
/>
</mat-form-field>
<div class="row">
<mat-radio-group [formControl]="form.controls.SongType">
<mat-radio-button value="Praise">Lobpreis</mat-radio-button>
<mat-radio-button value="Worship">Anbetung</mat-radio-button>
</mat-radio-group>
<mat-form-field>
<input
matInput
placeholder="Nummer"
[formControl]="form.controls.Number"
/>
</mat-form-field>
<mat-form-field>
<mat-label>Tonart</mat-label>
<mat-select [formControl]="form.controls.Key">
<mat-option *ngFor="let key of keys" [value]="key">
{{ key }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="Tempo"
[formControl]="form.controls.Tempo"
/>
</mat-form-field>
</div>
<mat-form-field>
<textarea
matInput
placeholder="Liedtext"
[formControl]="form.controls.Text"
[matTextareaAutosize]="true"
></textarea>
</mat-form-field>
<mat-form-field>
<textarea
matInput
placeholder="Kommentare"
[formControl]="form.controls.Comments"
[matTextareaAutosize]="true"
></textarea>
</mat-form-field>
</form>

View File

@@ -0,0 +1,9 @@
form {
display: flex;
flex-direction: column;
.row {
display: grid;
grid-template-columns: 3fr 1fr 1fr 1fr;
grid-column-gap: 20px;
}
}

View File

@@ -0,0 +1,13 @@
import { FormGroup } from '@angular/forms';
import { Component, Input } from '@angular/core';
import { keys } from './keys';
@Component({
selector: 'app-song-form',
templateUrl: './song-form.component.html',
styleUrls: ['./song-form.component.less']
})
export class SongFormComponent {
@Input() public form: FormGroup;
public keys = keys;
}

View File

@@ -0,0 +1,22 @@
<div class="song-detail-container">
<mat-card class="mat-elevation-z8" *ngIf="form">
<mat-card-header>
<div mat-card-avatar>
<button mat-icon-button (click)="onBack()" color="warn">
<fa-icon [icon]="faArrow"></fa-icon>
</button>
</div>
<mat-card-title>Neuen Titel anlegen</mat-card-title>
<mat-card-subtitle>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<app-song-form [form]="form"></app-song-form>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="onClickEdit()">
<fa-icon [icon]="faSave"></fa-icon> Speichern
</button>
</mat-card-actions>
</mat-card>
</div>

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,34 @@
import { EditSongService } from './../../../data/edit-song.service';
import { FormGroup } from '@angular/forms';
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { faLongArrowAltLeft, faSave } from '@fortawesome/free-solid-svg-icons';
import { State } from 'src/app/data/state';
import { SongsService } from 'src/app/data/songs.service';
@Component({
selector: 'app-song-new',
templateUrl: './song-new.component.html',
styleUrls: ['./song-new.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SongNewComponent implements OnInit {
public faArrow = faLongArrowAltLeft;
public faSave = faSave;
public form: FormGroup;
constructor(
private editSongService: EditSongService,
private songsService: SongsService,
private change: ChangeDetectorRef
) { }
ngOnInit() {
this.form = this.editSongService.initEditForm(false);
this.change.markForCheck();
}
public onBack(): void {
this.songsService.state = State.list;
}
}

View File

@@ -1,8 +1,8 @@
<div class="song-detail-container">
<mat-card class="mat-elevation-z8" [@blend] *ngIf="selectedSongId !== 0">
<div class="song-detail-container" *ngIf="song">
<mat-card class="mat-elevation-z8">
<mat-card-header>
<div mat-card-avatar>
<button mat-icon-button (click)="onBack()">
<button mat-icon-button (click)="onBack()" color="warn">
<fa-icon [icon]="faArrow"></fa-icon>
</button>
</div>
@@ -15,7 +15,6 @@
<p *ngFor="let line of comments">{{ line }}</p>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="onClickDownload()">Herunterladen</button>
<button mat-button (click)="onClickEdit()">
<fa-icon [icon]="faEdit"></fa-icon> Bearbeiten
</button>

View File

@@ -2,3 +2,7 @@ p {
margin: 0;
min-height: 10px;
}
:host {
display: block;
}

View File

@@ -7,14 +7,13 @@ import {
import { faLongArrowAltLeft, faEdit } from '@fortawesome/free-solid-svg-icons';
import { Song } from 'src/app/models/song.model';
import { DownloadService } from 'src/app/data/download.service';
import { blend } from 'src/app/services/animation';
import { State } from 'src/app/data/state';
@Component({
selector: 'app-song',
templateUrl: './song.component.html',
styleUrls: ['./song.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [blend]
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SongComponent {
public song: Song;
@@ -48,14 +47,14 @@ export class SongComponent {
}
public onClickEdit(): void {
this.songService.edit = true;
this.songService.state = State.edit;
}
public get text(): string[] {
return this.song.Text ? this.song.Text.split(/\r?\n/) : [];
return this.song && this.song.Text ? this.song.Text.split(/\r?\n/) : [];
}
public get comments(): string[] {
return this.song.Comments ? this.song.Comments.split(/\r?\n/) : [];
return this.song && this.song.Comments ? this.song.Comments.split(/\r?\n/) : [];
}
}

View File

@@ -1,3 +1,4 @@
<app-table></app-table>
<app-song *ngIf="!songsService.edit"></app-song>
<app-song-edit *ngIf="songsService.edit"></app-song-edit>
<app-song [@blend] *ngIf="songsService.state === State.read"></app-song>
<app-song-edit [@blend] *ngIf="songsService.state === State.edit"></app-song-edit>
<app-song-new [@blend] *ngIf="songsService.state === State.new"></app-song-new>

View File

@@ -1,12 +1,16 @@
import { blend } from 'src/app/services/animation';
import { Component } from '@angular/core';
import { SongsService } from 'src/app/data/songs.service';
import { State } from 'src/app/data/state';
@Component({
selector: 'app-songs',
templateUrl: './songs.component.html',
styleUrls: ['./songs.component.less']
styleUrls: ['./songs.component.less'],
animations: [blend]
})
export class SongsComponent {
public State = State;
constructor(public songsService: SongsService) {
songsService.loadSongList();
}

View File

@@ -1,8 +1,9 @@
<div
class="page-container mat-elevation-z8"
[class.pinned]="selectedSongId !== 0"
[class.pinned]="songsService.state !== State.list"
>
<div class="table-container">
<button mat-icon-button (click)="onClickNew()" class="button-new"><fa-icon [icon]="faNew"></fa-icon></button>
<table
mat-table
[dataSource]="songsService.songs | async"

View File

@@ -10,4 +10,16 @@ table {
overflow: auto;
}
.button-new {
position: absolute;
right: 5px;
top: 5px;
font-size: 2em;
z-index: 1000;
color: #aaa;
&:hover {
color: #000;
}
}

View File

@@ -4,6 +4,8 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { State } from 'src/app/data/state';
import { faFileMedical } from '@fortawesome/free-solid-svg-icons';
@Component({
selector: 'app-table',
@@ -13,10 +15,12 @@ import {
})
export class TableComponent {
public selectedSongId = 0;
public State = State;
public faNew = faFileMedical;
public columnsFull = ['Number', 'Name', 'Key', 'SongType', 'Tempo'];
public columnsPinned = ['Number', 'Name'];
public get columns(): string[] {
return this.selectedSongId === 0 ? this.columnsFull : this.columnsPinned;
return this.songsService.state === State.list ? this.columnsFull : this.columnsPinned;
}
constructor(
@@ -42,4 +46,11 @@ export class TableComponent {
this.songsService.selectSong(id);
this.change.detectChanges();
}
public onClickNew(): void {
this.songsService.selectSong(null);
this.songsService.state = State.new;
this.change.detectChanges();
}
}

View File

@@ -1,7 +1,8 @@
import { SongsService } from 'src/app/data/songs.service';
import { Injectable } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { switchMap, tap } from 'rxjs/operators';
import { switchMap } from 'rxjs/operators';
import { Song } from '../models/song.model';
@Injectable({
providedIn: 'root'
@@ -10,7 +11,7 @@ export class EditSongService {
constructor(private songsService: SongsService) {}
public initEditForm(attachSync: boolean): FormGroup {
const song = this.songsService.selectedSong.value;
const song = attachSync ? this.songsService.selectedSong.value : this.defaultValues();
const form = new FormGroup({
Number: new FormControl(song.Number, {
updateOn: 'blur',
@@ -25,7 +26,10 @@ export class EditSongService {
updateOn: 'blur',
validators: Validators.required
}),
Key: new FormControl(song.Key, { updateOn: 'blur' }),
Key: new FormControl(song.Key, {
updateOn: 'blur',
validators: Validators.required
}),
Tempo: new FormControl(song.Tempo, { updateOn: 'blur' }),
Comments: new FormControl(song.Comments, { updateOn: 'blur' }),
});
@@ -35,10 +39,33 @@ export class EditSongService {
return form;
}
private attachSync(form: FormGroup, song: import("/Users/benjamin/src/wgenerator/WEB/src/app/models/song.model").Song) {
private attachSync(form: FormGroup, song: Song) {
const controls = Object.keys(form.controls);
controls.forEach(control => {
form.controls[control].valueChanges.pipe(switchMap(value => this.songsService.patch(song.ID, control, value))).subscribe();
});
}
private defaultValues(): Song {
const song: Song = {
ID: null,
Number: this.firstFreeNumber(),
Name: null,
Tempo: null,
Text: null,
SongType: null,
Key: null,
Comments: null,
Final: false
};
return song;
}
private firstFreeNumber(): number {
let number = 0;
const numbers = this.songsService.songs.value.map(_ => _.Number);
while (numbers.indexOf(++number) !== -1) { }
return number;
}
}

View File

@@ -4,12 +4,13 @@ import { OdataService } from './odata.service';
import { Song } from '../models/song.model';
import { BehaviorSubject, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { State } from './state';
@Injectable({
providedIn: 'root'
})
export class SongsService extends OdataService {
public edit = false;
public state = State.list;
public songs: BehaviorSubject<Song[]> = new BehaviorSubject<Song[]>([]);
public selectedSong: BehaviorSubject<Song> = new BehaviorSubject<Song>(null);
@@ -24,7 +25,7 @@ export class SongsService extends OdataService {
}
public selectSong(id: number): void {
this.edit = false;
this.state = State.read;
const filter = this.songs.value.filter(_ => _.ID === id);
const song = filter.length === 1 ? filter[0] : null;
if (!song) {
@@ -39,6 +40,7 @@ export class SongsService extends OdataService {
}
public resetSelectedSong() {
this.state = State.list;
this.selectedSong.next(null);
}

View File

@@ -0,0 +1,6 @@
export enum State {
read,
edit,
new,
list
}

View File

@@ -2,12 +2,12 @@ import { trigger, transition, style, animate } from '@angular/animations';
export const blend = trigger('blend', [
transition(':enter', [
style({ opacity: 0 }),
animate('200ms', style({ opacity: 0 })),
animate('300ms', style({ opacity: 1 }))
style({ opacity: 0, display: 'none' }),
animate('400ms', style({ opacity: 0 , display: 'none'})),
animate('300ms', style({ opacity: 1, display: 'block' }))
]),
transition(':leave', [
style({ opacity: 1 }),
animate('300ms', style({ opacity: 0 }))
style({ opacity: 1, display: 'block' }),
animate('300ms', style({ opacity: 0, display: 'none' }))
])
]);

View File

@@ -36,6 +36,7 @@ html {
transition: all 300ms ease-in-out;
}
tbody {
tr.selected {
background-color: #0002;
}
@@ -49,6 +50,7 @@ html {
td.mat-cell {
padding: 0 5px;
}
}
&.pinned {
top: 0;