Skip to content

Commit

Permalink
Merge pull request #226 from e-picsa/refactor/form-base-components
Browse files Browse the repository at this point in the history
Refactor: form base components
  • Loading branch information
chrismclarke authored Feb 1, 2024
2 parents 607e00c + dda1443 commit cea14f4
Show file tree
Hide file tree
Showing 48 changed files with 420 additions and 659 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { PICSAFormValidators } from '@picsa/forms';
// eslint-disable-next-line @nx/enforce-module-boundaries
import type { Database } from '@picsa/server-types';
import { PICSAFormValidators } from '@picsa/shared/modules/forms/validators';
import {
IUploadResult,
SupabaseStoragePickerDirective,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { PicsaFormsModule } from '@picsa/forms';
import { PicsaTranslateModule } from '@picsa/shared/modules';

import { CropProbabilityTableComponent } from './crop-probability-table/crop-probability-table.component';
import { CropSelectComponent } from './crop-select/crop-select.component';
import { CropProbabilityMaterialModule } from './material.module';
import { CropProbabilityStationSelectComponent } from './station-select/station-select.component';

const components = [CropProbabilityStationSelectComponent, CropProbabilityTableComponent, CropSelectComponent];
const components = [CropProbabilityStationSelectComponent, CropProbabilityTableComponent];

@NgModule({
imports: [CommonModule, FormsModule, CropProbabilityMaterialModule, PicsaTranslateModule],
imports: [CommonModule, FormsModule, CropProbabilityMaterialModule, PicsaTranslateModule, PicsaFormsModule],
exports: [...components, CropProbabilityMaterialModule],
declarations: components,
providers: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<crop-probability-crop-select
[options]="cropOptions"
selectDefault="maize"
[stationId]="station.id"
(cropSelected)="filterData($event)"
<picsa-form-crop-select
[filterFn]="cropFilterFn"
(selectedChange)="filterData($event)"
[(ngModel)]="selectedCropName"
style="margin: 1rem"
></crop-probability-crop-select>
[resetOption]="{ text: 'Show All', matIcon: 'apps' }"
></picsa-form-crop-select>

<section class="table-container mat-mdc-table">
<!-- Main table -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { CROPS_DATA, ICropData } from '@picsa/data';
import { ICropData } from '@picsa/data';
import { arrayToHashmap } from '@picsa/utils';

import { IStationCropData, IStationCropDataItem, IStationCropInformation } from '../../models';
Expand All @@ -19,30 +19,30 @@ export class CropProbabilityTableComponent {

public dataSource: MatTableDataSource<ITableRow>;
public station: IStationCropInformation;
public selectedCropName?: string;
public cropOptions: ICropData[] = [];
public selectedCropName = 'maize';

private tableData: ITableRow[] = [];

@Input() set activeStation(activeStation: IStationCropInformation) {
this.station = activeStation;
this.tableData = this.prepareTableRows(activeStation);
this.filterData('');
this.filterData(this.selectedCropName);
}

constructor(private router: Router, private route: ActivatedRoute) {}

public handleStationChange() {
this.router.navigate([], { relativeTo: this.route, queryParams: { stationId: this.station?.id } });
}
public cropFilterFn: (option: ICropData) => boolean;

filterData(cropName = '') {
this.selectedCropName = cropName;
// flatten data rows which are grouped by crop
const dataSource = new MatTableDataSource(this.tableData);
// apply custom filter to avoid partial matches (e.g. soya-beans matching beans)
dataSource.filterPredicate = (data, filter) => data.crop.toLowerCase() === filter;
this.cropOptions = this.generateCropFilters(this.station.station_data);
this.generateCropFilters(this.station.station_data);
if (cropName) {
dataSource.filter = cropName.toLowerCase();
}
Expand All @@ -57,7 +57,7 @@ export class CropProbabilityTableComponent {
/** Generate list of crops for filtering that exist in the data */
private generateCropFilters(stationData: IStationCropData[]) {
const availableCrops = arrayToHashmap(stationData, 'crop');
return CROPS_DATA.filter(({ name }) => name in availableCrops);
this.cropFilterFn = ({ name }) => name in availableCrops;
}

/**
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
mat-button
*ngFor="let option of selectOptions"
class="select-button"
(click)="toggleOptionSelect(option.id)"
(click)="toggleSelected(option.id)"
[attr.data-gender]="option.id"
[attr.data-selected]="selected.includes(option.id)"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
forwardRef,
Input,
Output,
Provider,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, Provider } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker';
import { PicsaFormBaseSelectMultipleComponent } from '@picsa/forms/components/base/select-multiple';

const GENDER_OPTIONS: { [id: string]: { label: string; svgIcon: string } } = {
female: {
Expand All @@ -25,6 +17,8 @@ const GENDER_OPTIONS: { [id: string]: { label: string; svgIcon: string } } = {
/** Mark additional hardcoded strings for translation */
const STRINGS = { only: translateMarker('Only'), and: translateMarker('and'), both: translateMarker('Both') };

const SELECT_OPTIONS = Object.entries(GENDER_OPTIONS).map(([id, value]) => ({ ...value, id }));

/** Accessor used for binding with ngModel or formgroups */
export const GENDER_INPUT_CONTROL_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
Expand All @@ -34,17 +28,10 @@ export const GENDER_INPUT_CONTROL_VALUE_ACCESSOR: Provider = {

/**
* Custom input element designed for use with angular Ng-model or standalone syntax
*
* @example
* ```
* <option-gender-input [(ngModel)]="someVariable"></option-gender-input>
* // or
* <option-gender-input [selected]="someValue" (selectedChange)="handleChange()"></option-gender-input>
* ```
* Adapted from:
* https://valor-software.com/articles/avoiding-common-pitfalls-with-controlvalueaccessors-in-angular
* https://sreyaj.dev/custom-form-controls-controlvalueaccessor-in-angular
* https://indepth.dev/posts/1055/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms
*/
@Component({
selector: 'option-gender-input',
Expand All @@ -53,35 +40,16 @@ export const GENDER_INPUT_CONTROL_VALUE_ACCESSOR: Provider = {
providers: [GENDER_INPUT_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GenderInputComponent implements ControlValueAccessor {
protected selectOptions = Object.entries(GENDER_OPTIONS).map(([id, value]) => ({ ...value, id }));
export class GenderInputComponent extends PicsaFormBaseSelectMultipleComponent<typeof SELECT_OPTIONS[0]> {
// public override selectOptions = SELECT_OPTIONS;

/** Configurable display options */
@Input() options: { showValueText?: boolean; readonly?: boolean } = {};

/** Selected value binding */
@Input()
get selected() {
return this._selected;
}
set selected(selected: string[]) {
if (selected && this.selected.join(',') !== selected.join(',')) {
this._selected = selected.sort();
this.cdr.markForCheck();
this.selectedChange.emit(this._selected);
if (this._onChange) {
this._onChange(this._selected);
}
}
constructor(cdr: ChangeDetectorRef) {
super(cdr, SELECT_OPTIONS);
}

/** Additional event emitter to allow manual bind to <gender-input (selectedChange) /> event*/
@Output() selectedChange = new EventEmitter<string[]>();

private _selected: string[] = []; // this is the updated value that the class accesses

constructor(private cdr: ChangeDetectorRef) {}

/**
* Return a text representation of the value. Returns an array or individual words for easier translation
* In case one gender selected will return ['Only', 'Female']
Expand All @@ -93,33 +61,4 @@ export class GenderInputComponent implements ControlValueAccessor {
const [selectedId] = this.selected;
return [STRINGS.only, GENDER_OPTIONS[selectedId].label];
}

toggleOptionSelect(id: string) {
if (id) {
const valueIndex = this.selected.indexOf(id);
if (valueIndex === -1) {
this.selected = [...this._selected, id];
} else {
this.selected = this._selected.filter((v) => v !== id);
}
}
}

/** Events registered by ngModel and Form Controls */
// eslint-disable-next-line @typescript-eslint/member-ordering
private _onChange: (value: string[]) => void;
// eslint-disable-next-line @typescript-eslint/member-ordering
private _onTouched: (value: string[]) => void;

writeValue(selected: string[]) {
this.selected = selected;
}

registerOnChange(fn: (value: string[]) => void) {
this._onChange = fn;
}

registerOnTouched(fn: (value: string[]) => void) {
this._onTouched = fn; // <-- save the function
}
}
Loading

0 comments on commit cea14f4

Please sign in to comment.