Newer
Older
import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { Store, select } from '@ngrx/store';
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { GlobalState } from '@app/core/state';
import * as utils from '@app/degree-planner/shared/utils';
import * as selectors from '@app/degree-planner/store/selectors';
// API
import { DegreePlannerApiService } from '@app/degree-planner/services/api.service';
// Forms
import { FormBuilder, FormGroup, AbstractControl } from '@angular/forms';
// snackBar
import { MatSnackBar } from '@angular/material';
// Data modles
import { Course, SubjectMapping } from '@app/core/models/course';
@Component({
selector: 'cse-course-search',
templateUrl: './course-search.component.html',
styleUrls: ['./course-search.component.scss'],
})
export class CourseSearchComponent implements OnInit, OnDestroy {
// Internal variables used to store search info
public queriedCourses: Course[];
public hasResults: boolean;
public isLoading: boolean;
public courseSearchForm: FormGroup;
// Observable used for drag and drop and for populating term select
public dropZoneIds$: Observable<string[]>;
public activeTerms$: Observable<string[]>;
public activeSelectedSearchTerm$: Observable<string>;
public isCourseSearchOpen$: Observable<boolean>;
// Internal variables to store subjects and filtered subjects list
public subjects: string[];
public subjectMap: SubjectMapping;
public filteredSubjects: string[];
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public termSubscription: Subscription;
public searchOpenSubscribe: Subscription;
constructor(
private store: Store<GlobalState>,
private fb: FormBuilder,
private api: DegreePlannerApiService,
private snackBar: MatSnackBar,
) {}
public ngOnInit(): void {
// Internal values used to manage loading state
this.queriedCourses = [];
this.hasResults = false;
this.isLoading = false;
// Get active term drop zones
this.dropZoneIds$ = this.store.pipe(
select(selectors.selectAllVisibleYears),
utils.yearsToDropZoneIds(),
distinctUntilChanged(utils.compareStringArrays),
);
// Get active terms to populate term select box
this.activeTerms$ = this.store.pipe(select(selectors.getActiveTerms));
// Get observable for the search open state
this.isCourseSearchOpen$ = this.store.pipe(
select(selectors.isCourseSearchOpen),
);
// Reset serach form when closed
this.searchOpenSubscribe = this.isCourseSearchOpen$.subscribe(openState => {
if (!openState) {
this.resetSearchForm();
}
});
// Get all subjects and set them to internal values
this.store
.pipe(select(selectors.getSubjectDescriptions))
.subscribe(subjects => {
this.subjectMap = { [-1]: 'All', ...subjects };
const ordered = Object.entries(subjects)
.map(subject => {
return subject[1];
})
.sort();
this.subjects = ['all', ...ordered];
this.filteredSubjects = ['all', ...ordered];
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
});
// Deafults for the search form
this.courseSearchForm = this.fb.group({
term: '0000',
subject: 'All',
search: '',
});
// TODO Unsubscribe from this on component disconnect
// Get observable for currently active term
this.activeSelectedSearchTerm$ = this.store.pipe(
select(selectors.getActiveSelectedSearchTerm),
);
// Listen for changes on the active selected term and update the form when it changes
this.termSubscription = this.activeSelectedSearchTerm$.subscribe(
termCode => {
const values = this.courseSearchForm.value;
this.courseSearchForm.setValue({ ...values, term: termCode });
},
);
// Listen for changes to the term value
(this.courseSearchForm.get(
'term',
) as AbstractControl).valueChanges.subscribe(term => {
if (this.hasResults) {
const { search, subject } = this.courseSearchForm.value;
this.search(search, term, subject);
}
});
// Listen for changes in the subject input
(this.courseSearchForm.get(
'subject',
) as AbstractControl).valueChanges.subscribe(value => {

Scott Berg
committed
// If the subject value is blank, or if the user is typing "all"
if (/^(a[l]{0,2})?$/i.test(value.trim())) {
this.filteredSubjects = this.subjects;
return;
}
// Make both comparisons lowercase and remove all whitespace
const formattedSubject = subject.replace(/\s/g, '').toLowerCase();
const formattedValue = value.replace(/\s/g, '').toLowerCase();
if (formattedSubject.indexOf(formattedValue) === 0) {
});
});
}
// Unsubscribe from subs to prevent memeory leaks
public ngOnDestroy() {
this.termSubscription.unsubscribe();
this.searchOpenSubscribe.unsubscribe();
}
public subjectChange($event, input) {
console.log(input);
const subject = $event.source.value;
const { search, term } = this.courseSearchForm.value;
this.search(search, term, subject);
// Focus is needed to keep the cursor at the start of the input after a selection is made
input.focus();
}
// Get a subject code from text
public getSubjectCode(text): string | undefined {
// Check if subject is valid
const subjectEntry: [string, any] | undefined = Object.entries(
this.subjectMap,
).find(option => option[1].toLowerCase() === text.toLowerCase());
return subjectEntry ? subjectEntry[0] : subjectEntry;
}
// Function that runs on form submit
// Get form values and run a search
public formSubmit() {
const { search, term, subject } = this.courseSearchForm.value;
this.search(search, term, subject);
}
// Run a search and display all results if the serach is valid
public search(search, term, subject) {
// Get the form field values
// Check if subject is valid
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
if (!subjectCode) {
this.snackBar.open('Please select a valid subject', undefined, {
duration: 6000,
});
return;
}
// Set the internal UI state
this.isLoading = true;
this.hasResults = false;
this.queriedCourses = [];
// Hit the search API
this.api
.searchCourses({
subjectCode,
searchText: search,
termCode: term === '' ? '0000' : term,
})
.toPromise()
.then(res => {
// TODO add error handeling
// Update the internal state
this.hasResults = true;
this.isLoading = false;
// Map out the results and update the course object to match the needed structure
this.queriedCourses = res.hits.map(course => {
return {
...course,
subject: course.subject.description,
subjectCode: course.subject.subjectCode,
});
})
.catch(console.log);
}
// Reset the search form values and clear results
public resetSearchForm() {
this.queriedCourses = [];
this.hasResults = false;
// Ensure that the search form has been created before attempting to set values
if (this.courseSearchForm) {
this.courseSearchForm.setValue({
subject: 'All',
search: '',
term: '0000',
});
}
}
}