Skip to content

Commit

Permalink
Merge pull request #149 from mobi/go_file_upload
Browse files Browse the repository at this point in the history
Go File Upload
  • Loading branch information
grahamhency authored Oct 31, 2019
2 parents e09ea73 + 63c737d commit 0ec0d50
Show file tree
Hide file tree
Showing 16 changed files with 817 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Directive, EventEmitter, HostBinding, HostListener, Input, Output } from '@angular/core';

@Directive({
selector: '[goDragonDrop]'
})
export class DragonDropDirective {
active: boolean = false;
@Input() class: string = ''; // override the standard class attr with a new one.
@Input() activeClass: string = '';
@Output() dropped: EventEmitter<any> = new EventEmitter<any>();

@HostBinding('class')
get hostClasses(): string {
return [
this.class,
this.active ? this.activeClass : '',
].join(' ');
}

// Dragover listener
@HostListener('dragover', ['$event']) onDragOver(evt: Event): void {
evt.preventDefault();
evt.stopPropagation();
this.active = true;
}

// Dragleave listener
@HostListener('dragleave', ['$event']) public onDragLeave(evt: Event): void {
evt.preventDefault();
evt.stopPropagation();
this.active = false;
}

// Drop listener
@HostListener('drop', ['$event']) public ondrop(evt: any): void {
evt.preventDefault();
evt.stopPropagation();
this.active = false;
this.dropped.emit(evt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<label class="go-form__label"
[attr.for]="id"
[ngClass]="{'go-form__label--dark': theme === 'dark'}">
{{ label }}
</label>
<div [ngClass]="{ 'go-file-upload--disabled': state === 'selected' || isLoading, 'go-file-upload--dark': theme === 'dark' }">
<div
goDragonDrop
class="go-file-upload go-file-upload__container"
activeClass="go-file-upload--active"
(click)="filePicker.click()"
(dropped)="onFilePicked($event)"
>
<div class="go-file-upload__container" *ngIf="state === 'selecting' && !isLoading">
<go-icon icon="publish" iconModifier="large"></go-icon>
Drag and Drop File or Click to Browse
</div>
<div class="go-file-upload__container go-file-upload__container--loading" *ngIf="isLoading">
<go-loader *ngIf="isLoading" loaderSize="small"></go-loader>
<span class="go-file-upload__container--loading-text">Uploading File...</span>
</div>
<div class="go-file-upload__container go-file-upload__container--selected" *ngIf="state === 'selected' && !isLoading">
<go-icon icon="check" class="go-file-upload__selected-checkmark" iconClass="go-icon--large"></go-icon>
File Selected
</div>
<input
type="file"
[id]="id"
#filePicker
class="go-file-upload__input"
(change)="onFilePicked($event.target.files)"
[attr.multiple]="multiple ? true : null">
</div>
</div>

<div *ngIf="filePreview.length" class="go-file-upload__file-name">
<label class="go-form__label file-name__label" [ngClass]="{'go-form__label--dark': theme === 'dark'}" *ngIf="multiple">Files</label>
<label class="go-form__label file-name__label" [ngClass]="{'go-form__label--dark': theme === 'dark'}" *ngIf="!multiple">File Name</label>
<p *ngFor="let file of filePreview; let i = index" class="go-file-upload__file-list">
<span class="go-file-upload__file-name" [ngClass]="{'go-file-upload__file-name--dark': theme === 'dark'}">{{ file }}</span>
<go-icon-button buttonIcon="close" (handleClick)="removeFile(i)"></go-icon-button>
</p>
</div>

<go-hint *ngFor="let hint of hints" [message]="hint" [theme]="theme">
</go-hint>

<div *ngIf="control?.errors?.length">
<go-hint
*ngFor="let error of control.errors"
[message]="error.message"
[label]="error.type"
[theme]="theme"
type="negative"
></go-hint>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
@import '../../../styles/variables';

@mixin upload-active {
cursor: pointer;
background-color: $theme-light-bg-hover;
opacity: 0.8;
}

.go-file-upload__input {
visibility: hidden;
}

.go-file-upload {
height: 10rem;
border: 1px dashed $theme-light-border;
border-radius: $global-radius;
}

.go-file-upload--disabled {
color: lighten($theme-light-color, 40);
cursor: not-allowed;
pointer-events: none;
border-style: solid;
}

.go-file-upload--dark {
border: 1px dashed $theme-dark-border;
background-color: $theme-dark-bg;
border-radius: $global-radius;
color: $theme-dark-color;

.go-file-upload:hover {
background-color: $theme-dark-bg-hover;
color: $theme-dark-color-hover
}

.go-file-upload--active {
color: $theme-dark-color-hover;
background-color: $theme-dark-bg-hover;
}
}

.go-file-upload:hover {
@include upload-active;
}

.go-file-upload--active {
@include upload-active;
border-style: solid;
}

.go-file-upload__container {
justify-content: center;
display: flex;
flex-direction: column;
text-align: center;
}

.go-file-upload__file-list {
display: flex;
justify-content: space-between;
}

.go-file-upload__file-name {
margin-top: .5rem;
padding-right: .5rem;
}

.go-file-upload__container--loading-text {
padding-top: .5rem;
}

.go-file-upload__container--loading {
padding-top: 1rem;
}

.file-name__label {
padding-bottom: .25rem;
}

.go-file-upload__file-name--dark {
color: $theme-dark-color;
}

.go-file-upload__selected-checkmark {
color: $ui-color-positive;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { GoFileUploadComponent } from './go-file-upload.component';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormArray, FormGroup, FormControl } from '@angular/forms';
import { GoButtonModule } from '../go-button/go-button.module';
import { GoHintModule } from '../go-hint/go-hint.module';
import { GoIconButtonModule } from '../go-icon-button/go-icon-button.module';
import { GoIconModule } from '../go-icon/go-icon.module';
import { GoLoaderModule } from '../go-loader/go-loader.module';
import { createHostListener } from '@angular/compiler/src/core';

describe('GoFileUploadComponent', () => {
let component: GoFileUploadComponent;
let fixture: ComponentFixture<GoFileUploadComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GoFileUploadComponent],
imports: [
CommonModule,
FormsModule,
GoButtonModule,
GoHintModule,
GoIconModule,
GoIconButtonModule,
GoLoaderModule,
ReactiveFormsModule
]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(GoFileUploadComponent);
component = fixture.componentInstance;
component.control = new FormControl('');
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

describe('removeFile', () => {
beforeEach(() => {
const file: FormGroup = new FormGroup({
file: new FormControl('whatever')
});
component.files = new FormArray([file]);
component.filePreview = ['whatever'];
});

it('should set state to selecting if it was previously selected', () => {
component.state = 'selected';
component.removeFile(0);
expect(component.state).toBe('selecting');
});

it('should not change the state if the state was selecting', () => {
component.state = 'selecting';
component.removeFile(0);
expect(component.state).toBe('selecting');
});

it('should remove the file at the given index from the form control', () => {
component.removeFile(0);
expect(component.control.value.length).toBe(0);
});

it('should remove the file at the given index from our file Preview Array', () => {
component.removeFile(0);
expect(component.filePreview.length).toBe(0);
});
});

describe('ngOnInit()', () => {
beforeEach(() => {
component.id = undefined;
});

it('sets the id of the input to `key` if one is passed in', () => {
expect(component.id).toBeUndefined();

component.key = 'a-specific-id';
component.ngOnInit();

expect(component.id).toBe(component.key);
});

it('generates a semi-random id based on the label if key is undefined', () => {
expect(component.key).toBeUndefined();
expect(component.id).toBeUndefined();

component.label = 'test label';
component.ngOnInit();

expect(component.id).toBeDefined();
expect(component.id).toContain('test-label-');
});

it('generates a semi-random id if a label and key are undefined', () => {
expect(component.key).toBeUndefined();
expect(component.label).toBeUndefined();
expect(component.id).toBeUndefined();

component.ngOnInit();

expect(component.id).toBeDefined();
expect(component.id).toContain('file-upload-');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms';

@Component({
selector: 'go-file-upload',
templateUrl: './go-file-upload.component.html',
styleUrls: ['./go-file-upload.component.scss']
})
export class GoFileUploadComponent implements OnInit {
form: FormGroup;
files: FormArray;
fb: FormBuilder;
filePreview: Array<string> = [];
id: string;

@Input() control: FormControl;
@Input() hints: Array<string> = [];
@Input() isLoading: boolean = false;
@Input() key: string;
@Input() label: string;
@Input() multiple: boolean = false;
@Input() state: 'selecting' | 'selected' = 'selecting';
@Input() theme: 'light' | 'dark' = 'light';

constructor() { }

ngOnInit(): void {
this.id = this.key || this.generateId(this.label);
this.fb = new FormBuilder();
this.form = this.fb.group({
filesArray: this.fb.array([])
});
this.files = <FormArray>this.form.controls['filesArray'];
this.files.valueChanges.subscribe( (fileChanges: any) => {
this.control.setValue(fileChanges);
});
}

onFilePicked(evt: any): void {
const files: File[] = evt.dataTransfer.files;
if (files.length > 0) {
Array.from(files).forEach((file: any) => {
this.files.push(this.patchValues(file));
this.filePreview.push(file.name);
});
if (!this.multiple) {
this.state = 'selected';
}
}
}

removeFile(index: number): void {
this.files.removeAt(index);
this.filePreview.splice(index, 1);
if (this.state === 'selected') {
this.state = 'selecting';
}
}

private patchValues(file: any): AbstractControl {
return this.fb.group({
file: [file]
});
}

private generateId(label: string): string {
const labelText: string = label || 'file-upload';
const idArray: Array<string> = labelText.split(' ');

// NOTE: There is only a one in a million chance that this number is not unique.
idArray.push(String(Math.round(Math.random() * 1000000)));

return idArray.join('-');
}
}
Loading

0 comments on commit 0ec0d50

Please sign in to comment.