Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/image select #268

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## Unreleased

## [5.1.0] - 2022-01-10

### Added
- `forms: image-select` Added the image-select component.


## [5.1.1] - 2021-11-30

Expand Down
70 changes: 70 additions & 0 deletions packages/ngx-forms/src/lib/image-select/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# @acpaas-ui/ngx-forms

The image select is a multiple select component with every selectable option being an image.

## Usage

```typescript
import { ImageSelectModule } from '@acpaas-ui/ngx-forms';
```

## Documentation

Visit our [documentation site](https://antwerp-ui.digipolis.be/) for full how-to docs and guidelines

### API

| Name | Default value | Description |
| ----------- | ------ | -------------------------- |
| `@Input() choices: ImageSelectChoices[];` | - | Available choices. |
| `@Input() maxSelectable: number;` | - | The number of choices a user can maximally select. |


### Example

```typescript
import { ImageSelectModule } from '@acpaas-ui/ngx-forms';

@NgModule({
imports: [
ImageSelectModule,
]
});

export class AppModule {};
```

#### Basic

```typescript
import { ImageSelectChoice } from '@acpaas-ui/ngx-forms';

public fruits: ImageSelectChoice[] = [
{
label: 'Kiwi',
key: 'kiwi',
alt: 'Kiwi',
url: 'url to image here'
},
{
label: 'Apple',
key: 'apple',
alt: 'Apple',
url: 'url to image here'
},
{
label: 'Raspberry',
key: 'raspberry',
alt: 'Raspberry',
url: 'url to image here'
},
];
```

```html
<aui-image-select [choices]="fruits" [maxSelectable]="2"></aui-image-select>
```

## Contributing

Visit our [Contribution Guidelines](../../../../../CONTRIBUTING.md) for more information on how to contribute.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class="row">
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-3 col-xl-2 u-margin-bottom-xs"
*ngFor="let choice of choices; let i = index">

<input [id]="'option-' + i"
type="checkbox"
(change)="toggleSelected(choice, $event)"
[disabled]="isDisabled"/>
<label [for]="'option-' + i">
<img [src]="choice.url" [alt]="choice.alt" class="m-image">
<p>{{ choice.label }}</p>
</label>

</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@import '~@a-ui/core/dist/assets/styles/quarks';
@import '~@a-ui/flexboxgrid/dist/flexboxgrid.min.css';

@keyframes o-image-select__selection-animation {
from {
border: 3px solid transparent;
}
to {
border: 3px solid $brand-primary;
background-color: $selection-color;
}
}

:host {
input[type="checkbox"] {
display: none;
}

label {

width: 100%;
height: 100%;

display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;

border: 3px solid transparent;

cursor: pointer;

img {
object-fit: cover;
max-width: 100%;
}
}

:checked + label {
animation: o-image-select__selection-animation $animation-normal;

border: 3px solid $brand-primary;
background-color: $selection-color;
}

:disabled + label,
&.is-max-checked :not(:checked) + label{
cursor: default;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { ImageSelectComponent } from './image-select.component';
import { ImageSelectChoice } from '../../types/image-select.types';


describe('The ImageSelect Component', () => {
let comp: ImageSelectComponent;
let kiwi: ImageSelectChoice;
let apple: ImageSelectChoice;
let raspberry: ImageSelectChoice;
let cherry: ImageSelectChoice;
let pear: ImageSelectChoice;
let fixture: ComponentFixture<ImageSelectComponent>;
let de: DebugElement;
let el: HTMLElement;

// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ImageSelectComponent], // declare the test component
})
.compileComponents(); // compile template and css
}));

// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(ImageSelectComponent);

comp = fixture.componentInstance; // ImageSelectComponent test instance
kiwi = {
label: 'Kiwi',
key: 'kiwi',
alt: 'Kiwi',
// tslint:disable-next-line:max-line-length
url: 'https://images.unsplash.com/photo-1591796079433-7f41b45eb95c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2004&q=80'
};
apple = {
label: 'Apple',
key: 'apple',
alt: 'Apple',
// tslint:disable-next-line:max-line-length
url: 'https://images.unsplash.com/photo-1619546813926-a78fa6372cd2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80'
};
raspberry = {
label: 'Raspberry',
key: 'raspberry',
alt: 'Raspberry',
// tslint:disable-next-line:max-line-length
url: 'https://images.unsplash.com/photo-1577069861033-55d04cec4ef5?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1064&q=80'
};
cherry = {
label: 'Cherry',
key: 'cherry',
alt: 'Cherry',
// tslint:disable-next-line:max-line-length
url: 'https://images.unsplash.com/photo-1528821154947-1aa3d1b74941?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80'
};
pear = {
label: 'Pear',
key: 'pear',
alt: 'Pear',
// tslint:disable-next-line:max-line-length
url: 'https://images.unsplash.com/photo-1548199569-3e1c6aa8f469?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=992&q=80'
};
comp.choices = [kiwi, apple, raspberry, cherry, pear];

// query for the row containing images by CSS element selector
de = fixture.debugElement.query(By.css('.row'));
el = de.nativeElement;
});

it('should exist', () => {
fixture.detectChanges();
expect(el).not.toBeUndefined();
});

it('ngOnInit must initialize maxSelectable', () => {
spyOn(comp, 'updateModel');
comp.maxSelectable = undefined;
comp.ngOnInit();

expect(comp.maxSelectable).toBe(comp.choices.length);
});

describe('toggleSelected', () => {

let event;

beforeEach(() => {
event = {
target: {
checked: true
}
};
});

it('should update the model', () => {
spyOn(comp, 'updateModel');
fixture.detectChanges();

comp.toggleSelected(apple, event);

expect(comp.selectedImageKeys).toEqual(['apple']);
expect(comp.updateModel).toHaveBeenCalledWith(['apple']);
});

it('should add the selected image key if it was not in the selectedImageKeys array and update the model value', () => {
comp.selectedImageKeys = ['apple'];

spyOn(comp, 'updateModel').and.stub();
fixture.detectChanges();

comp.toggleSelected(pear, event);

expect(comp.selectedImageKeys).toEqual(['apple', 'pear']);
expect(comp.updateModel).toHaveBeenCalledWith(['apple', 'pear']);
});

it('should remove the selected image key if it was present in the selectedItems array and update the model value', () => {
comp.selectedImageKeys = ['apple', 'pear', 'kiwi'];

spyOn(comp, 'updateModel').and.stub();
fixture.detectChanges();

comp.toggleSelected(pear, event);

expect(comp.selectedImageKeys).toEqual(['apple', 'kiwi']);
expect(comp.updateModel).toHaveBeenCalledWith(['apple', 'kiwi']);
});

it('should not do anything on selecting when max selectable is reached', () => {
comp.selectedImageKeys = ['apple', 'pear'];
comp.maxSelectable = 2;

spyOn(comp, 'updateModel').and.stub();
fixture.detectChanges();

comp.toggleSelected(kiwi, event);

expect(comp.maxCheckedClass).toBe(true);
expect(event.target.checked).toBe(false);
expect(comp.selectedImageKeys).toEqual(['apple', 'pear']);
expect(comp.updateModel).not.toHaveBeenCalled();
});

it('should be able to deselect when max selectable is reached', () => {
comp.selectedImageKeys = ['apple', 'pear'];
comp.maxSelectable = 2;

spyOn(comp, 'updateModel').and.stub();
fixture.detectChanges();

comp.toggleSelected(apple, event);

expect(comp.selectedImageKeys).toEqual(['pear',]);
expect(comp.updateModel).toHaveBeenCalledWith(['pear']);
});
});
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Component, forwardRef, HostBinding, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ImageSelectChoice } from '../../types/image-select.types';

@Component({
selector: 'aui-image-select',
templateUrl: './image-select.component.html',
styleUrls: ['./image-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ImageSelectComponent),
multi: true,
}],
})
export class ImageSelectComponent implements ControlValueAccessor, OnInit {
@Input() public choices: ImageSelectChoice[];
@Input() public maxSelectable: number | undefined;

public selectedImageKeys: string[] = [];
public isDisabled = false;

public ngOnInit(): void {
if (this.maxSelectable === undefined) {
this.maxSelectable = this.choices.length;
}
}

@HostBinding('class.is-max-checked') get maxCheckedClass(): boolean {
return this.isMaxNumberSelected();
}

public updateModel: (_) => any = () => {
}

public registerOnChange(onChange: (_) => any): void {
this.updateModel = onChange;
}

public registerOnTouched(): void {
}

public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ik zie hier nergens styling voor, dus er is nu volgens mij geen onderscheid met een enabled optie.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Css toegevoegd zodat bij disabled state een standaard cursor wordt getoond en geen 'pointer'.

}

writeValue(value: string[]): void {
this.selectedImageKeys = Array.isArray(value) ? value : [];
}

toggleSelected(choice: ImageSelectChoice, $event: any): void {
if (this.isSelected(choice)) {
this.unselect(choice);
} else if (!this.isMaxNumberSelected()) {
this.select(choice);
} else {
// Uncheck checkbox when max number of selections made
$event.target.checked = false;
}
}

private isSelected(choice: ImageSelectChoice): boolean {
return !!this.selectedImageKeys.find(selectedKey => selectedKey === choice.key);
}

private isMaxNumberSelected(): boolean {
return this.selectedImageKeys.length === this.maxSelectable;
}

private unselect(choice: ImageSelectChoice): void {
const index = this.selectedImageKeys.indexOf(choice.key);
this.selectedImageKeys = [
...this.selectedImageKeys.slice(0, index),
...this.selectedImageKeys.slice(index + 1),
];
this.updateModel(this.selectedImageKeys);
}

private select(choice: ImageSelectChoice): void {
this.selectedImageKeys = this.selectedImageKeys.concat(choice.key);
this.updateModel(this.selectedImageKeys);
}
}
Loading