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

feat(checkbox): add component #61

Merged
merged 2 commits into from
Jul 3, 2024
Merged
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
490 changes: 475 additions & 15 deletions projects/cli/documentation.json

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion projects/cli/schematics/components/components-generator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export interface ComponentGeneratorSchema {
components: ('avatar' | 'badge' | 'button' | 'pin' | 'switch' | 'tag')[];
components: (
| 'avatar'
| 'badge'
| 'button'
| 'checkbox'
| 'pin'
| 'switch'
| 'tag'
)[];
path: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<input
#inputElement
[attr.aria-checked]="checked()"
[checked]="checked()"
[attr.aria-disabled]="disabled()"
[disabled]="disabled()"
(change)="onToggle()"
(keydown.enter)="onToggle()"
type="checkbox"
/>
@switch (checked()) {
@case (false) {
}
@case (true) {
}
@case ('mixed') {
-
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Define SCSS variables for better maintainability
$checkbox-size: 22px;
$transition-duration: 0.4s;
$checkbox-bg-color: white;
$checkbox-checked-bg-color: green;
$checkbox-border-color: grey;
$checkbox-disabled-bg-color: lightgrey;
$checkbox-check-color: white;

/* Hide the default checkbox */
input[type='checkbox'] {
position: absolute;
opacity: 0;
cursor: pointer;
height: 100%;
width: 100%;
padding: 0;
margin: 0;
top: 0;
left: 0;
}

:host {
display: inline-block;
height: $checkbox-size;
width: $checkbox-size;
position: relative;
text-align: center;
vertical-align: middle;
background-color: $checkbox-bg-color;
border: 2px solid $checkbox-border-color;
border-radius: 4px;
transition:
background-color $transition-duration,
border-color $transition-duration;
user-select: none;

}

/* DISABLED STATE */
:host[disabled],
:host:has([disabled]) {
background-color: $checkbox-disabled-bg-color;
border-color: $checkbox-disabled-bg-color;
opacity: 0.6;

* {
cursor: not-allowed;
}
}

/* CHECKED STATE */
:host:has(input[type='checkbox'][aria-checked='true']) {
background-color: $checkbox-checked-bg-color;
border-color: $checkbox-checked-bg-color;
}

/* UNCHECKED STATE */
:host:has(input[type='checkbox'][aria-checked='false']) {
background-color: white;
}

/* MIXED/INDETERMINATE STATE */
:host:has(input[type='checkbox'][aria-checked='mixed']) {
background-color: lightgray;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ZenCheckboxComponent } from './checkbox.component';

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

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ZenCheckboxComponent],
}).compileComponents();

fixture = TestBed.createComponent(ZenCheckboxComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
DestroyRef,
ElementRef,
forwardRef,
inject,
model,
Renderer2,
viewChild,
} from '@angular/core';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import {
ControlValueAccessor,
FormsModule,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { map } from 'rxjs';

type CheckedState = boolean | 'mixed';
type OnChangeFn = (value: CheckedState) => void;
type OnTouchedFn = () => void;

/**
* ZenCheckboxComponent is a custom checkbox component implementing ControlValueAccessor.
* It supports a tri-state checkbox (checked, unchecked, and indeterminate).
*
* @example
* <zen-checkbox />
*
* @export
* @class ZenCheckboxComponent
* @implements {ControlValueAccessor}
* @implements {AfterViewInit}
*
* @license BSD-2-Clause
* @author Konrad Stępień
* @link https://github.com/Kordrad/ng-zen
*/
@Component({
selector: 'zen-checkbox',
standalone: true,
templateUrl: './checkbox.component.html',
styleUrl: './checkbox.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ZenCheckboxComponent),
multi: true,
},
],
imports: [FormsModule],
})
export class ZenCheckboxComponent
implements ControlValueAccessor, AfterViewInit
{
/** Model for the checked state of the checkbox. */
readonly checked = model<CheckedState>(false);

/** Model for the disabled state of the checkbox. */
readonly disabled = model<boolean>(false);

/** @ignore */
private readonly checked$ = toObservable(this.checked);

/** @ignore */
private readonly destroyRef = inject(DestroyRef);
/** @ignore */
private readonly renderer2 = inject(Renderer2);

/** @ignore */
private readonly inputElement =
viewChild<ElementRef<HTMLInputElement>>('inputElement');

/**
* Lifecycle hook called after Angular has fully initialized a component's view.
* Initializes the indeterminate state of the checkbox.
*
* @ignore
*/
ngAfterViewInit(): void {
this.initIndeterminate();
}

private onChange: OnChangeFn = () => {};
private onTouched: OnTouchedFn = () => {};

/**
* Writes a new value to the component.
*/
writeValue(value: boolean): void {
this.checked.set(value);
}

/**
* Registers a function to be called when the value changes.
*/
registerOnChange(fn: OnChangeFn): void {
this.onChange = fn;
}

/**
* Registers a function to be called when the component is touched.
*/
registerOnTouched(fn: OnTouchedFn): void {
this.onTouched = fn;
}

/**
* Sets the disabled state of the component.
*/
setDisabledState(isDisabled: boolean): void {
this.disabled.set(isDisabled);
}

/**
* Toggles the checkbox value and notifies the change.
* If the component is disabled, no action is performed.
*/
onToggle(): void {
if (this.disabled()) return;

this.checked.update(value => !value);
this.onChange(this.checked());
this.onTouched();
}

/**
* Initializes the indeterminate state of the checkbox based on the checked state.
* If the checked state is 'mixed', the checkbox will be set to indeterminate.
*/
private initIndeterminate(): void {
this.checked$
.pipe(
takeUntilDestroyed(this.destroyRef),
map((value: CheckedState) => value === 'mixed')
)
.subscribe((value: boolean) => {
this.renderer2.setProperty(
this.inputElement()?.nativeElement,
'indeterminate',
value
);
});
}
}
10 changes: 10 additions & 0 deletions projects/cli/schematics/components/files/checkbox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';

import { ZenCheckboxComponent } from './checkbox.component';

@NgModule({
imports: [ZenCheckboxComponent],
exports: [ZenCheckboxComponent],
})
export class ZenCheckboxModule {}
export * from './checkbox.component';
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { NgIf } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Expand Down Expand Up @@ -40,7 +39,7 @@ type OnTouchedFn = () => void;
multi: true,
},
],
imports: [FormsModule, NgIf],
imports: [FormsModule],
})
export class ZenSwitchComponent implements ControlValueAccessor {
/** Model for the checked state of the switch. */
Expand Down
10 changes: 9 additions & 1 deletion projects/cli/schematics/components/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@
"type": "array",
"items": {
"type": "string",
"enum": ["avatar", "badge", "button", "pin", "switch", "tag"]
"enum": [
"avatar",
"badge",
"button",
"checkbox",
"pin",
"switch",
"tag"
]
},
"multiselect": true,
"x-prompt": "Which component should be generated?"
Expand Down
32 changes: 32 additions & 0 deletions projects/cli/stories/components/checkbox.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/angular';

import { ZenCheckboxComponent } from '../../schematics/components/files/checkbox/checkbox.component';

export default {
title: 'Components/Checkbox',
component: ZenCheckboxComponent,
tags: ['autodocs'],
render: args => ({ props: { ...args } }),
} satisfies Meta<ZenCheckboxComponent>;

type Story = StoryObj<ZenCheckboxComponent>;

export const Default: Story = {
render: () => ({
template: `
<zen-checkbox />
<zen-checkbox [checked]="true" />
<zen-checkbox checked="mixed" />
`,
}),
};

export const Diabled: Story = {
render: () => ({
template: `
<zen-checkbox disabled="true"/>
<zen-checkbox [checked]="true" disabled="true"/>
<zen-checkbox checked="mixed" disabled="true"/>
`,
}),
};
Loading