feat: Implement tasks feature using NGRX signals and remove the old counter store, alongside general project configuration and skill documentation updates.
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
This commit is contained in:
@@ -12,22 +12,22 @@ Create custom directives for reusable DOM manipulation and behavior in Angular v
|
||||
Modify the appearance or behavior of an element:
|
||||
|
||||
```typescript
|
||||
import { Directive, input, effect, inject, ElementRef } from "@angular/core";
|
||||
import { Directive, input, effect, inject, ElementRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: "[appHighlight]",
|
||||
selector: '[appHighlight]',
|
||||
})
|
||||
export class Highlight {
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
|
||||
// Input with alias matching selector
|
||||
color = input("yellow", { alias: "appHighlight" });
|
||||
// Input with alias matching selector
|
||||
color = input('yellow', { alias: 'appHighlight' });
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.el.nativeElement.style.backgroundColor = this.color();
|
||||
});
|
||||
}
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.el.nativeElement.style.backgroundColor = this.color();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <p appHighlight="lightblue">Highlighted text</p>
|
||||
@@ -40,39 +40,39 @@ Prefer `host` over `@HostBinding`/`@HostListener`:
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appTooltip]",
|
||||
host: {
|
||||
"(mouseenter)": "show()",
|
||||
"(mouseleave)": "hide()",
|
||||
"[attr.aria-describedby]": "tooltipId",
|
||||
},
|
||||
selector: '[appTooltip]',
|
||||
host: {
|
||||
'(mouseenter)': 'show()',
|
||||
'(mouseleave)': 'hide()',
|
||||
'[attr.aria-describedby]': 'tooltipId',
|
||||
},
|
||||
})
|
||||
export class Tooltip {
|
||||
text = input.required<string>({ alias: "appTooltip" });
|
||||
position = input<"top" | "bottom" | "left" | "right">("top");
|
||||
text = input.required<string>({ alias: 'appTooltip' });
|
||||
position = input<'top' | 'bottom' | 'left' | 'right'>('top');
|
||||
|
||||
tooltipId = `tooltip-${crypto.randomUUID()}`;
|
||||
private tooltipEl: HTMLElement | null = null;
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
tooltipId = `tooltip-${crypto.randomUUID()}`;
|
||||
private tooltipEl: HTMLElement | null = null;
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
|
||||
show() {
|
||||
this.tooltipEl = document.createElement("div");
|
||||
this.tooltipEl.id = this.tooltipId;
|
||||
this.tooltipEl.className = `tooltip tooltip-${this.position()}`;
|
||||
this.tooltipEl.textContent = this.text();
|
||||
this.tooltipEl.setAttribute("role", "tooltip");
|
||||
document.body.appendChild(this.tooltipEl);
|
||||
this.positionTooltip();
|
||||
}
|
||||
show() {
|
||||
this.tooltipEl = document.createElement('div');
|
||||
this.tooltipEl.id = this.tooltipId;
|
||||
this.tooltipEl.className = `tooltip tooltip-${this.position()}`;
|
||||
this.tooltipEl.textContent = this.text();
|
||||
this.tooltipEl.setAttribute('role', 'tooltip');
|
||||
document.body.appendChild(this.tooltipEl);
|
||||
this.positionTooltip();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.tooltipEl?.remove();
|
||||
this.tooltipEl = null;
|
||||
}
|
||||
hide() {
|
||||
this.tooltipEl?.remove();
|
||||
this.tooltipEl = null;
|
||||
}
|
||||
|
||||
private positionTooltip() {
|
||||
// Position logic based on this.position() and this.el
|
||||
}
|
||||
private positionTooltip() {
|
||||
// Position logic based on this.position() and this.el
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <button appTooltip="Click to save" position="bottom">Save</button>
|
||||
@@ -82,21 +82,21 @@ export class Tooltip {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appButton]",
|
||||
host: {
|
||||
class: "btn",
|
||||
"[class.btn-primary]": 'variant() === "primary"',
|
||||
"[class.btn-secondary]": 'variant() === "secondary"',
|
||||
"[class.btn-sm]": 'size() === "small"',
|
||||
"[class.btn-lg]": 'size() === "large"',
|
||||
"[class.disabled]": "disabled()",
|
||||
"[attr.disabled]": "disabled() || null",
|
||||
},
|
||||
selector: '[appButton]',
|
||||
host: {
|
||||
'class': 'btn',
|
||||
'[class.btn-primary]': 'variant() === "primary"',
|
||||
'[class.btn-secondary]': 'variant() === "secondary"',
|
||||
'[class.btn-sm]': 'size() === "small"',
|
||||
'[class.btn-lg]': 'size() === "large"',
|
||||
'[class.disabled]': 'disabled()',
|
||||
'[attr.disabled]': 'disabled() || null',
|
||||
},
|
||||
})
|
||||
export class Button {
|
||||
variant = input<"primary" | "secondary">("primary");
|
||||
size = input<"small" | "medium" | "large">("medium");
|
||||
disabled = input(false, { transform: booleanAttribute });
|
||||
variant = input<'primary' | 'secondary'>('primary');
|
||||
size = input<'small' | 'medium' | 'large'>('medium');
|
||||
disabled = input(false, { transform: booleanAttribute });
|
||||
}
|
||||
|
||||
// Usage: <button appButton variant="primary" size="large">Click</button>
|
||||
@@ -106,21 +106,21 @@ export class Button {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appClickOutside]",
|
||||
host: {
|
||||
"(document:click)": "onDocumentClick($event)",
|
||||
},
|
||||
selector: '[appClickOutside]',
|
||||
host: {
|
||||
'(document:click)': 'onDocumentClick($event)',
|
||||
},
|
||||
})
|
||||
export class ClickOutside {
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
|
||||
clickOutside = output<void>();
|
||||
clickOutside = output<void>();
|
||||
|
||||
onDocumentClick(event: MouseEvent) {
|
||||
if (!this.el.nativeElement.contains(event.target as Node)) {
|
||||
this.clickOutside.emit();
|
||||
onDocumentClick(event: MouseEvent) {
|
||||
if (!this.el.nativeElement.contains(event.target as Node)) {
|
||||
this.clickOutside.emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <div appClickOutside (clickOutside)="closeMenu()">...</div>
|
||||
@@ -130,32 +130,30 @@ export class ClickOutside {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appShortcut]",
|
||||
host: {
|
||||
"(document:keydown)": "onKeydown($event)",
|
||||
},
|
||||
selector: '[appShortcut]',
|
||||
host: {
|
||||
'(document:keydown)': 'onKeydown($event)',
|
||||
},
|
||||
})
|
||||
export class Shortcut {
|
||||
key = input.required<string>({ alias: "appShortcut" });
|
||||
ctrl = input(false, { transform: booleanAttribute });
|
||||
shift = input(false, { transform: booleanAttribute });
|
||||
alt = input(false, { transform: booleanAttribute });
|
||||
key = input.required<string>({ alias: 'appShortcut' });
|
||||
ctrl = input(false, { transform: booleanAttribute });
|
||||
shift = input(false, { transform: booleanAttribute });
|
||||
alt = input(false, { transform: booleanAttribute });
|
||||
|
||||
triggered = output<KeyboardEvent>();
|
||||
triggered = output<KeyboardEvent>();
|
||||
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
const keyMatch = event.key.toLowerCase() === this.key().toLowerCase();
|
||||
const ctrlMatch = this.ctrl()
|
||||
? event.ctrlKey || event.metaKey
|
||||
: !event.ctrlKey && !event.metaKey;
|
||||
const shiftMatch = this.shift() ? event.shiftKey : !event.shiftKey;
|
||||
const altMatch = this.alt() ? event.altKey : !event.altKey;
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
const keyMatch = event.key.toLowerCase() === this.key().toLowerCase();
|
||||
const ctrlMatch = this.ctrl() ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey;
|
||||
const shiftMatch = this.shift() ? event.shiftKey : !event.shiftKey;
|
||||
const altMatch = this.alt() ? event.altKey : !event.altKey;
|
||||
|
||||
if (keyMatch && ctrlMatch && shiftMatch && altMatch) {
|
||||
event.preventDefault();
|
||||
this.triggered.emit(event);
|
||||
if (keyMatch && ctrlMatch && shiftMatch && altMatch) {
|
||||
event.preventDefault();
|
||||
this.triggered.emit(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <button appShortcut="s" ctrl (triggered)="save()">Save (Ctrl+S)</button>
|
||||
@@ -170,46 +168,38 @@ Use structural directives for DOM manipulation beyond control flow (portals, ove
|
||||
Render content in a different DOM location:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Directive,
|
||||
inject,
|
||||
TemplateRef,
|
||||
ViewContainerRef,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
import { Directive, inject, TemplateRef, ViewContainerRef, OnInit, OnDestroy, input } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: "[appPortal]",
|
||||
selector: '[appPortal]',
|
||||
})
|
||||
export class Portal implements OnInit, OnDestroy {
|
||||
private templateRef = inject(TemplateRef<any>);
|
||||
private viewContainerRef = inject(ViewContainerRef);
|
||||
private viewRef: EmbeddedViewRef<any> | null = null;
|
||||
private templateRef = inject(TemplateRef<any>);
|
||||
private viewContainerRef = inject(ViewContainerRef);
|
||||
private viewRef: EmbeddedViewRef<any> | null = null;
|
||||
|
||||
// Target container selector or element
|
||||
target = input<string | HTMLElement>("body", { alias: "appPortal" });
|
||||
// Target container selector or element
|
||||
target = input<string | HTMLElement>('body', { alias: 'appPortal' });
|
||||
|
||||
ngOnInit() {
|
||||
const container = this.getContainer();
|
||||
if (container) {
|
||||
this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef);
|
||||
this.viewRef.rootNodes.forEach((node) => container.appendChild(node));
|
||||
ngOnInit() {
|
||||
const container = this.getContainer();
|
||||
if (container) {
|
||||
this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef);
|
||||
this.viewRef.rootNodes.forEach((node) => container.appendChild(node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.viewRef?.destroy();
|
||||
}
|
||||
|
||||
private getContainer(): HTMLElement | null {
|
||||
const target = this.target();
|
||||
if (typeof target === "string") {
|
||||
return document.querySelector(target);
|
||||
ngOnDestroy() {
|
||||
this.viewRef?.destroy();
|
||||
}
|
||||
|
||||
private getContainer(): HTMLElement | null {
|
||||
const target = this.target();
|
||||
if (typeof target === 'string') {
|
||||
return document.querySelector(target);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: Render modal at body level
|
||||
@@ -224,24 +214,24 @@ Defer rendering until condition is met (one-time):
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appLazyRender]",
|
||||
selector: '[appLazyRender]',
|
||||
})
|
||||
export class LazyRender {
|
||||
private templateRef = inject(TemplateRef<any>);
|
||||
private viewContainer = inject(ViewContainerRef);
|
||||
private rendered = false;
|
||||
private templateRef = inject(TemplateRef<any>);
|
||||
private viewContainer = inject(ViewContainerRef);
|
||||
private rendered = false;
|
||||
|
||||
condition = input.required<boolean>({ alias: "appLazyRender" });
|
||||
condition = input.required<boolean>({ alias: 'appLazyRender' });
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
// Only render once when condition becomes true
|
||||
if (this.condition() && !this.rendered) {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
this.rendered = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
constructor() {
|
||||
effect(() => {
|
||||
// Only render once when condition becomes true
|
||||
if (this.condition() && !this.rendered) {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
this.rendered = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: Render heavy component only when tab is first activated
|
||||
@@ -254,44 +244,44 @@ export class LazyRender {
|
||||
|
||||
```typescript
|
||||
interface TemplateContext<T> {
|
||||
$implicit: T;
|
||||
item: T;
|
||||
index: number;
|
||||
$implicit: T;
|
||||
item: T;
|
||||
index: number;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: "[appTemplateOutlet]",
|
||||
selector: '[appTemplateOutlet]',
|
||||
})
|
||||
export class TemplateOutlet<T> {
|
||||
private viewContainer = inject(ViewContainerRef);
|
||||
private currentView: EmbeddedViewRef<TemplateContext<T>> | null = null;
|
||||
private viewContainer = inject(ViewContainerRef);
|
||||
private currentView: EmbeddedViewRef<TemplateContext<T>> | null = null;
|
||||
|
||||
template = input.required<TemplateRef<TemplateContext<T>>>({
|
||||
alias: "appTemplateOutlet",
|
||||
});
|
||||
context = input.required<T>({ alias: "appTemplateOutletContext" });
|
||||
index = input(0, { alias: "appTemplateOutletIndex" });
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const template = this.template();
|
||||
const context = this.context();
|
||||
const index = this.index();
|
||||
|
||||
if (this.currentView) {
|
||||
this.currentView.context.$implicit = context;
|
||||
this.currentView.context.item = context;
|
||||
this.currentView.context.index = index;
|
||||
this.currentView.markForCheck();
|
||||
} else {
|
||||
this.currentView = this.viewContainer.createEmbeddedView(template, {
|
||||
$implicit: context,
|
||||
item: context,
|
||||
index,
|
||||
});
|
||||
}
|
||||
template = input.required<TemplateRef<TemplateContext<T>>>({
|
||||
alias: 'appTemplateOutlet',
|
||||
});
|
||||
}
|
||||
context = input.required<T>({ alias: 'appTemplateOutletContext' });
|
||||
index = input(0, { alias: 'appTemplateOutletIndex' });
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const template = this.template();
|
||||
const context = this.context();
|
||||
const index = this.index();
|
||||
|
||||
if (this.currentView) {
|
||||
this.currentView.context.$implicit = context;
|
||||
this.currentView.context.item = context;
|
||||
this.currentView.context.index = index;
|
||||
this.currentView.markForCheck();
|
||||
} else {
|
||||
this.currentView = this.viewContainer.createEmbeddedView(template, {
|
||||
$implicit: context,
|
||||
item: context,
|
||||
index,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: Custom list with template
|
||||
@@ -310,64 +300,64 @@ Compose directives on components or other directives:
|
||||
```typescript
|
||||
// Reusable behavior directives
|
||||
@Directive({
|
||||
selector: "[focusable]",
|
||||
host: {
|
||||
tabindex: "0",
|
||||
"(focus)": "onFocus()",
|
||||
"(blur)": "onBlur()",
|
||||
"[class.focused]": "isFocused()",
|
||||
},
|
||||
selector: '[focusable]',
|
||||
host: {
|
||||
'tabindex': '0',
|
||||
'(focus)': 'onFocus()',
|
||||
'(blur)': 'onBlur()',
|
||||
'[class.focused]': 'isFocused()',
|
||||
},
|
||||
})
|
||||
export class Focusable {
|
||||
isFocused = signal(false);
|
||||
isFocused = signal(false);
|
||||
|
||||
onFocus() {
|
||||
this.isFocused.set(true);
|
||||
}
|
||||
onBlur() {
|
||||
this.isFocused.set(false);
|
||||
}
|
||||
onFocus() {
|
||||
this.isFocused.set(true);
|
||||
}
|
||||
onBlur() {
|
||||
this.isFocused.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: "[disableable]",
|
||||
host: {
|
||||
"[class.disabled]": "disabled()",
|
||||
"[attr.aria-disabled]": "disabled()",
|
||||
},
|
||||
selector: '[disableable]',
|
||||
host: {
|
||||
'[class.disabled]': 'disabled()',
|
||||
'[attr.aria-disabled]': 'disabled()',
|
||||
},
|
||||
})
|
||||
export class Disableable {
|
||||
disabled = input(false, { transform: booleanAttribute });
|
||||
disabled = input(false, { transform: booleanAttribute });
|
||||
}
|
||||
|
||||
// Component using host directives
|
||||
@Component({
|
||||
selector: "app-custom-button",
|
||||
hostDirectives: [
|
||||
Focusable,
|
||||
{
|
||||
directive: Disableable,
|
||||
inputs: ["disabled"],
|
||||
selector: 'app-custom-button',
|
||||
hostDirectives: [
|
||||
Focusable,
|
||||
{
|
||||
directive: Disableable,
|
||||
inputs: ['disabled'],
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'role': 'button',
|
||||
'(click)': 'onClick($event)',
|
||||
'(keydown.enter)': 'onClick($event)',
|
||||
'(keydown.space)': 'onClick($event)',
|
||||
},
|
||||
],
|
||||
host: {
|
||||
role: "button",
|
||||
"(click)": "onClick($event)",
|
||||
"(keydown.enter)": "onClick($event)",
|
||||
"(keydown.space)": "onClick($event)",
|
||||
},
|
||||
template: `<ng-content />`,
|
||||
template: `<ng-content />`,
|
||||
})
|
||||
export class CustomButton {
|
||||
private disableable = inject(Disableable);
|
||||
private disableable = inject(Disableable);
|
||||
|
||||
clicked = output<void>();
|
||||
clicked = output<void>();
|
||||
|
||||
onClick(event: Event) {
|
||||
if (!this.disableable.disabled()) {
|
||||
this.clicked.emit();
|
||||
onClick(event: Event) {
|
||||
if (!this.disableable.disabled()) {
|
||||
this.clicked.emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <app-custom-button disabled>Click me</app-custom-button>
|
||||
@@ -377,38 +367,38 @@ export class CustomButton {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[hoverable]",
|
||||
host: {
|
||||
"(mouseenter)": "onEnter()",
|
||||
"(mouseleave)": "onLeave()",
|
||||
"[class.hovered]": "isHovered()",
|
||||
},
|
||||
selector: '[hoverable]',
|
||||
host: {
|
||||
'(mouseenter)': 'onEnter()',
|
||||
'(mouseleave)': 'onLeave()',
|
||||
'[class.hovered]': 'isHovered()',
|
||||
},
|
||||
})
|
||||
export class Hoverable {
|
||||
isHovered = signal(false);
|
||||
isHovered = signal(false);
|
||||
|
||||
hoverChange = output<boolean>();
|
||||
hoverChange = output<boolean>();
|
||||
|
||||
onEnter() {
|
||||
this.isHovered.set(true);
|
||||
this.hoverChange.emit(true);
|
||||
}
|
||||
onEnter() {
|
||||
this.isHovered.set(true);
|
||||
this.hoverChange.emit(true);
|
||||
}
|
||||
|
||||
onLeave() {
|
||||
this.isHovered.set(false);
|
||||
this.hoverChange.emit(false);
|
||||
}
|
||||
onLeave() {
|
||||
this.isHovered.set(false);
|
||||
this.hoverChange.emit(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-card",
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: Hoverable,
|
||||
outputs: ["hoverChange"],
|
||||
},
|
||||
],
|
||||
template: `<ng-content />`,
|
||||
selector: 'app-card',
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: Hoverable,
|
||||
outputs: ['hoverChange'],
|
||||
},
|
||||
],
|
||||
template: `<ng-content />`,
|
||||
})
|
||||
export class Card {}
|
||||
|
||||
@@ -421,31 +411,31 @@ Combine multiple behaviors:
|
||||
|
||||
```typescript
|
||||
// Base directives
|
||||
@Directive({ selector: "[withRipple]" })
|
||||
@Directive({ selector: '[withRipple]' })
|
||||
export class Ripple {
|
||||
// Ripple effect implementation
|
||||
// Ripple effect implementation
|
||||
}
|
||||
|
||||
@Directive({ selector: "[withElevation]" })
|
||||
@Directive({ selector: '[withElevation]' })
|
||||
export class Elevation {
|
||||
elevation = input(2);
|
||||
elevation = input(2);
|
||||
}
|
||||
|
||||
// Composed component
|
||||
@Component({
|
||||
selector: "app-material-button",
|
||||
hostDirectives: [
|
||||
Ripple,
|
||||
{
|
||||
directive: Elevation,
|
||||
inputs: ["elevation"],
|
||||
},
|
||||
{
|
||||
directive: Disableable,
|
||||
inputs: ["disabled"],
|
||||
},
|
||||
],
|
||||
template: `<ng-content />`,
|
||||
selector: 'app-material-button',
|
||||
hostDirectives: [
|
||||
Ripple,
|
||||
{
|
||||
directive: Elevation,
|
||||
inputs: ['elevation'],
|
||||
},
|
||||
{
|
||||
directive: Disableable,
|
||||
inputs: ['disabled'],
|
||||
},
|
||||
],
|
||||
template: `<ng-content />`,
|
||||
})
|
||||
export class MaterialButton {}
|
||||
```
|
||||
|
||||
@@ -15,23 +15,23 @@
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appAutoFocus]",
|
||||
selector: '[appAutoFocus]',
|
||||
})
|
||||
export class AutoFocus {
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
|
||||
enabled = input(true, { alias: "appAutoFocus", transform: booleanAttribute });
|
||||
delay = input(0);
|
||||
enabled = input(true, { alias: 'appAutoFocus', transform: booleanAttribute });
|
||||
delay = input(0);
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
if (this.enabled()) {
|
||||
setTimeout(() => {
|
||||
this.el.nativeElement.focus();
|
||||
}, this.delay());
|
||||
}
|
||||
});
|
||||
}
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
if (this.enabled()) {
|
||||
setTimeout(() => {
|
||||
this.el.nativeElement.focus();
|
||||
}, this.delay());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <input appAutoFocus />
|
||||
@@ -42,26 +42,26 @@ export class AutoFocus {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appSelectAll]",
|
||||
host: {
|
||||
"(focus)": "onFocus()",
|
||||
"(click)": "onClick($event)",
|
||||
},
|
||||
selector: '[appSelectAll]',
|
||||
host: {
|
||||
'(focus)': 'onFocus()',
|
||||
'(click)': 'onClick($event)',
|
||||
},
|
||||
})
|
||||
export class SelectAll {
|
||||
private el = inject(ElementRef<HTMLInputElement>);
|
||||
private el = inject(ElementRef<HTMLInputElement>);
|
||||
|
||||
onFocus() {
|
||||
// Delay to ensure value is set
|
||||
setTimeout(() => this.el.nativeElement.select(), 0);
|
||||
}
|
||||
|
||||
onClick(event: MouseEvent) {
|
||||
// Select all on first click if not already focused
|
||||
if (document.activeElement !== this.el.nativeElement) {
|
||||
this.el.nativeElement.select();
|
||||
onFocus() {
|
||||
// Delay to ensure value is set
|
||||
setTimeout(() => this.el.nativeElement.select(), 0);
|
||||
}
|
||||
|
||||
onClick(event: MouseEvent) {
|
||||
// Select all on first click if not already focused
|
||||
if (document.activeElement !== this.el.nativeElement) {
|
||||
this.el.nativeElement.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <input appSelectAll value="Select me on focus" />
|
||||
@@ -71,26 +71,26 @@ export class SelectAll {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appCopyToClipboard]",
|
||||
host: {
|
||||
"(click)": "copy()",
|
||||
"[style.cursor]": '"pointer"',
|
||||
},
|
||||
selector: '[appCopyToClipboard]',
|
||||
host: {
|
||||
'(click)': 'copy()',
|
||||
'[style.cursor]': '"pointer"',
|
||||
},
|
||||
})
|
||||
export class CopyToClipboard {
|
||||
text = input.required<string>({ alias: "appCopyToClipboard" });
|
||||
text = input.required<string>({ alias: 'appCopyToClipboard' });
|
||||
|
||||
copied = output<void>();
|
||||
error = output<Error>();
|
||||
copied = output<void>();
|
||||
error = output<Error>();
|
||||
|
||||
async copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.text());
|
||||
this.copied.emit();
|
||||
} catch (err) {
|
||||
this.error.emit(err as Error);
|
||||
async copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.text());
|
||||
this.copied.emit();
|
||||
} catch (err) {
|
||||
this.error.emit(err as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
@@ -105,24 +105,24 @@ export class CopyToClipboard {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "input[appTrim], textarea[appTrim]",
|
||||
host: {
|
||||
"(blur)": "onBlur()",
|
||||
},
|
||||
selector: 'input[appTrim], textarea[appTrim]',
|
||||
host: {
|
||||
'(blur)': 'onBlur()',
|
||||
},
|
||||
})
|
||||
export class Trim {
|
||||
private el = inject(ElementRef<HTMLInputElement | HTMLTextAreaElement>);
|
||||
private ngControl = inject(NgControl, { optional: true, self: true });
|
||||
private el = inject(ElementRef<HTMLInputElement | HTMLTextAreaElement>);
|
||||
private ngControl = inject(NgControl, { optional: true, self: true });
|
||||
|
||||
onBlur() {
|
||||
const value = this.el.nativeElement.value;
|
||||
const trimmed = value.trim();
|
||||
onBlur() {
|
||||
const value = this.el.nativeElement.value;
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (value !== trimmed) {
|
||||
this.el.nativeElement.value = trimmed;
|
||||
this.ngControl?.control?.setValue(trimmed);
|
||||
if (value !== trimmed) {
|
||||
this.el.nativeElement.value = trimmed;
|
||||
this.ngControl?.control?.setValue(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <input appTrim formControlName="name" />
|
||||
@@ -132,92 +132,88 @@ export class Trim {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appMask]",
|
||||
host: {
|
||||
"(input)": "onInput($event)",
|
||||
"(keydown)": "onKeydown($event)",
|
||||
},
|
||||
selector: '[appMask]',
|
||||
host: {
|
||||
'(input)': 'onInput($event)',
|
||||
'(keydown)': 'onKeydown($event)',
|
||||
},
|
||||
})
|
||||
export class Mask {
|
||||
private el = inject(ElementRef<HTMLInputElement>);
|
||||
private el = inject(ElementRef<HTMLInputElement>);
|
||||
|
||||
// Mask pattern: 9 = digit, A = letter, * = any
|
||||
mask = input.required<string>({ alias: "appMask" });
|
||||
// Mask pattern: 9 = digit, A = letter, * = any
|
||||
mask = input.required<string>({ alias: 'appMask' });
|
||||
|
||||
onInput(event: InputEvent) {
|
||||
const input = this.el.nativeElement;
|
||||
const value = input.value;
|
||||
const masked = this.applyMask(value);
|
||||
onInput(event: InputEvent) {
|
||||
const input = this.el.nativeElement;
|
||||
const value = input.value;
|
||||
const masked = this.applyMask(value);
|
||||
|
||||
if (value !== masked) {
|
||||
input.value = masked;
|
||||
}
|
||||
}
|
||||
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
// Allow navigation keys
|
||||
if (
|
||||
["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(
|
||||
event.key,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = this.el.nativeElement;
|
||||
const position = input.selectionStart ?? 0;
|
||||
const maskChar = this.mask()[position];
|
||||
|
||||
if (!maskChar) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidChar(event.key, maskChar)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private applyMask(value: string): string {
|
||||
const mask = this.mask();
|
||||
let result = "";
|
||||
let valueIndex = 0;
|
||||
|
||||
for (let i = 0; i < mask.length && valueIndex < value.length; i++) {
|
||||
const maskChar = mask[i];
|
||||
const inputChar = value[valueIndex];
|
||||
|
||||
if (maskChar === "9" || maskChar === "A" || maskChar === "*") {
|
||||
if (this.isValidChar(inputChar, maskChar)) {
|
||||
result += inputChar;
|
||||
valueIndex++;
|
||||
} else {
|
||||
valueIndex++;
|
||||
i--;
|
||||
if (value !== masked) {
|
||||
input.value = masked;
|
||||
}
|
||||
} else {
|
||||
result += maskChar;
|
||||
if (inputChar === maskChar) {
|
||||
valueIndex++;
|
||||
}
|
||||
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
// Allow navigation keys
|
||||
if (['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = this.el.nativeElement;
|
||||
const position = input.selectionStart ?? 0;
|
||||
const maskChar = this.mask()[position];
|
||||
|
||||
if (!maskChar) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidChar(event.key, maskChar)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
private applyMask(value: string): string {
|
||||
const mask = this.mask();
|
||||
let result = '';
|
||||
let valueIndex = 0;
|
||||
|
||||
private isValidChar(char: string, maskChar: string): boolean {
|
||||
switch (maskChar) {
|
||||
case "9":
|
||||
return /\d/.test(char);
|
||||
case "A":
|
||||
return /[a-zA-Z]/.test(char);
|
||||
case "*":
|
||||
return /[a-zA-Z0-9]/.test(char);
|
||||
default:
|
||||
return char === maskChar;
|
||||
for (let i = 0; i < mask.length && valueIndex < value.length; i++) {
|
||||
const maskChar = mask[i];
|
||||
const inputChar = value[valueIndex];
|
||||
|
||||
if (maskChar === '9' || maskChar === 'A' || maskChar === '*') {
|
||||
if (this.isValidChar(inputChar, maskChar)) {
|
||||
result += inputChar;
|
||||
valueIndex++;
|
||||
} else {
|
||||
valueIndex++;
|
||||
i--;
|
||||
}
|
||||
} else {
|
||||
result += maskChar;
|
||||
if (inputChar === maskChar) {
|
||||
valueIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private isValidChar(char: string, maskChar: string): boolean {
|
||||
switch (maskChar) {
|
||||
case '9':
|
||||
return /\d/.test(char);
|
||||
case 'A':
|
||||
return /[a-zA-Z]/.test(char);
|
||||
case '*':
|
||||
return /[a-zA-Z0-9]/.test(char);
|
||||
default:
|
||||
return char === maskChar;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <input appMask="(999) 999-9999" placeholder="(555) 123-4567" />
|
||||
@@ -227,29 +223,29 @@ export class Mask {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appCharCount]",
|
||||
selector: '[appCharCount]',
|
||||
})
|
||||
export class CharCount {
|
||||
private el = inject(ElementRef<HTMLInputElement | HTMLTextAreaElement>);
|
||||
private el = inject(ElementRef<HTMLInputElement | HTMLTextAreaElement>);
|
||||
|
||||
maxLength = input.required<number>({ alias: "appCharCount" });
|
||||
maxLength = input.required<number>({ alias: 'appCharCount' });
|
||||
|
||||
currentLength = signal(0);
|
||||
remaining = computed(() => this.maxLength() - this.currentLength());
|
||||
isOverLimit = computed(() => this.remaining() < 0);
|
||||
currentLength = signal(0);
|
||||
remaining = computed(() => this.maxLength() - this.currentLength());
|
||||
isOverLimit = computed(() => this.remaining() < 0);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.currentLength.set(this.el.nativeElement.value.length);
|
||||
});
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.currentLength.set(this.el.nativeElement.value.length);
|
||||
});
|
||||
|
||||
// Listen for input changes
|
||||
afterNextRender(() => {
|
||||
this.el.nativeElement.addEventListener("input", () => {
|
||||
this.currentLength.set(this.el.nativeElement.value.length);
|
||||
});
|
||||
});
|
||||
}
|
||||
// Listen for input changes
|
||||
afterNextRender(() => {
|
||||
this.el.nativeElement.addEventListener('input', () => {
|
||||
this.currentLength.set(this.el.nativeElement.value.length);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage with template:
|
||||
@@ -263,59 +259,59 @@ export class CharCount {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appLazyLoad]",
|
||||
selector: '[appLazyLoad]',
|
||||
})
|
||||
export class LazyLoad implements OnDestroy {
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
private observer: IntersectionObserver | null = null;
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
private observer: IntersectionObserver | null = null;
|
||||
|
||||
src = input.required<string>({ alias: "appLazyLoad" });
|
||||
placeholder = input("/assets/placeholder.png");
|
||||
src = input.required<string>({ alias: 'appLazyLoad' });
|
||||
placeholder = input('/assets/placeholder.png');
|
||||
|
||||
loaded = output<void>();
|
||||
loaded = output<void>();
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.setupObserver();
|
||||
});
|
||||
}
|
||||
|
||||
private setupObserver() {
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.loadImage();
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.setupObserver();
|
||||
});
|
||||
},
|
||||
{ rootMargin: "50px" },
|
||||
);
|
||||
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
|
||||
// Set placeholder
|
||||
if (this.el.nativeElement instanceof HTMLImageElement) {
|
||||
this.el.nativeElement.src = this.placeholder();
|
||||
}
|
||||
}
|
||||
|
||||
private loadImage() {
|
||||
const element = this.el.nativeElement;
|
||||
private setupObserver() {
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.loadImage();
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: '50px' },
|
||||
);
|
||||
|
||||
if (element instanceof HTMLImageElement) {
|
||||
element.src = this.src();
|
||||
element.onload = () => this.loaded.emit();
|
||||
} else {
|
||||
element.style.backgroundImage = `url(${this.src()})`;
|
||||
this.loaded.emit();
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
|
||||
// Set placeholder
|
||||
if (this.el.nativeElement instanceof HTMLImageElement) {
|
||||
this.el.nativeElement.src = this.placeholder();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
private loadImage() {
|
||||
const element = this.el.nativeElement;
|
||||
|
||||
if (element instanceof HTMLImageElement) {
|
||||
element.src = this.src();
|
||||
element.onload = () => this.loaded.emit();
|
||||
} else {
|
||||
element.style.backgroundImage = `url(${this.src()})`;
|
||||
this.loaded.emit();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: <img [appLazyLoad]="imageUrl" alt="Lazy loaded image" />
|
||||
@@ -325,49 +321,49 @@ export class LazyLoad implements OnDestroy {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appInfiniteScroll]",
|
||||
selector: '[appInfiniteScroll]',
|
||||
})
|
||||
export class InfiniteScroll implements OnDestroy {
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
private observer: IntersectionObserver | null = null;
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
private observer: IntersectionObserver | null = null;
|
||||
|
||||
threshold = input(0.1);
|
||||
disabled = input(false);
|
||||
threshold = input(0.1);
|
||||
disabled = input(false);
|
||||
|
||||
scrolled = output<void>();
|
||||
scrolled = output<void>();
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.setupObserver();
|
||||
});
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.setupObserver();
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (this.disabled()) {
|
||||
effect(() => {
|
||||
if (this.disabled()) {
|
||||
this.observer?.disconnect();
|
||||
} else {
|
||||
this.setupObserver();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupObserver() {
|
||||
this.observer?.disconnect();
|
||||
} else {
|
||||
this.setupObserver();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupObserver() {
|
||||
this.observer?.disconnect();
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && !this.disabled()) {
|
||||
this.scrolled.emit();
|
||||
}
|
||||
},
|
||||
{ threshold: this.threshold() },
|
||||
);
|
||||
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && !this.disabled()) {
|
||||
this.scrolled.emit();
|
||||
}
|
||||
},
|
||||
{ threshold: this.threshold() },
|
||||
);
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
}
|
||||
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
@@ -385,35 +381,35 @@ export class InfiniteScroll implements OnDestroy {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appResize]",
|
||||
selector: '[appResize]',
|
||||
})
|
||||
export class Resize implements OnDestroy {
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
private observer: ResizeObserver | null = null;
|
||||
private el = inject(ElementRef<HTMLElement>);
|
||||
private observer: ResizeObserver | null = null;
|
||||
|
||||
width = signal(0);
|
||||
height = signal(0);
|
||||
width = signal(0);
|
||||
height = signal(0);
|
||||
|
||||
resized = output<{ width: number; height: number }>();
|
||||
resized = output<{ width: number; height: number }>();
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
const { width, height } = entry.contentRect;
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
const { width, height } = entry.contentRect;
|
||||
|
||||
this.width.set(width);
|
||||
this.height.set(height);
|
||||
this.resized.emit({ width, height });
|
||||
});
|
||||
this.width.set(width);
|
||||
this.height.set(height);
|
||||
this.resized.emit({ width, height });
|
||||
});
|
||||
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
});
|
||||
}
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
@@ -426,75 +422,72 @@ export class Resize implements OnDestroy {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appDraggable]",
|
||||
host: {
|
||||
draggable: "true",
|
||||
"[class.dragging]": "isDragging()",
|
||||
"(dragstart)": "onDragStart($event)",
|
||||
"(dragend)": "onDragEnd($event)",
|
||||
},
|
||||
selector: '[appDraggable]',
|
||||
host: {
|
||||
'draggable': 'true',
|
||||
'[class.dragging]': 'isDragging()',
|
||||
'(dragstart)': 'onDragStart($event)',
|
||||
'(dragend)': 'onDragEnd($event)',
|
||||
},
|
||||
})
|
||||
export class Draggable {
|
||||
data = input<any>(null, { alias: "appDraggable" });
|
||||
effectAllowed = input<DataTransfer["effectAllowed"]>("move");
|
||||
data = input<any>(null, { alias: 'appDraggable' });
|
||||
effectAllowed = input<DataTransfer['effectAllowed']>('move');
|
||||
|
||||
isDragging = signal(false);
|
||||
isDragging = signal(false);
|
||||
|
||||
dragStart = output<DragEvent>();
|
||||
dragEnd = output<DragEvent>();
|
||||
dragStart = output<DragEvent>();
|
||||
dragEnd = output<DragEvent>();
|
||||
|
||||
onDragStart(event: DragEvent) {
|
||||
this.isDragging.set(true);
|
||||
onDragStart(event: DragEvent) {
|
||||
this.isDragging.set(true);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = this.effectAllowed();
|
||||
event.dataTransfer.setData(
|
||||
"application/json",
|
||||
JSON.stringify(this.data()),
|
||||
);
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = this.effectAllowed();
|
||||
event.dataTransfer.setData('application/json', JSON.stringify(this.data()));
|
||||
}
|
||||
|
||||
this.dragStart.emit(event);
|
||||
}
|
||||
|
||||
this.dragStart.emit(event);
|
||||
}
|
||||
|
||||
onDragEnd(event: DragEvent) {
|
||||
this.isDragging.set(false);
|
||||
this.dragEnd.emit(event);
|
||||
}
|
||||
onDragEnd(event: DragEvent) {
|
||||
this.isDragging.set(false);
|
||||
this.dragEnd.emit(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: "[appDropZone]",
|
||||
host: {
|
||||
"[class.drag-over]": "isDragOver()",
|
||||
"(dragover)": "onDragOver($event)",
|
||||
"(dragleave)": "onDragLeave($event)",
|
||||
"(drop)": "onDrop($event)",
|
||||
},
|
||||
selector: '[appDropZone]',
|
||||
host: {
|
||||
'[class.drag-over]': 'isDragOver()',
|
||||
'(dragover)': 'onDragOver($event)',
|
||||
'(dragleave)': 'onDragLeave($event)',
|
||||
'(drop)': 'onDrop($event)',
|
||||
},
|
||||
})
|
||||
export class DropZone {
|
||||
isDragOver = signal(false);
|
||||
isDragOver = signal(false);
|
||||
|
||||
dropped = output<any>();
|
||||
dropped = output<any>();
|
||||
|
||||
onDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
this.isDragOver.set(true);
|
||||
}
|
||||
|
||||
onDragLeave(event: DragEvent) {
|
||||
this.isDragOver.set(false);
|
||||
}
|
||||
|
||||
onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
this.isDragOver.set(false);
|
||||
|
||||
const data = event.dataTransfer?.getData("application/json");
|
||||
if (data) {
|
||||
this.dropped.emit(JSON.parse(data));
|
||||
onDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
this.isDragOver.set(true);
|
||||
}
|
||||
|
||||
onDragLeave(event: DragEvent) {
|
||||
this.isDragOver.set(false);
|
||||
}
|
||||
|
||||
onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
this.isDragOver.set(false);
|
||||
|
||||
const data = event.dataTransfer?.getData('application/json');
|
||||
if (data) {
|
||||
this.dropped.emit(JSON.parse(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
@@ -506,42 +499,42 @@ export class DropZone {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appHasPermission]",
|
||||
selector: '[appHasPermission]',
|
||||
})
|
||||
export class HasPermission {
|
||||
private templateRef = inject(TemplateRef<any>);
|
||||
private viewContainer = inject(ViewContainerRef);
|
||||
private authService = inject(Auth);
|
||||
private hasView = false;
|
||||
private templateRef = inject(TemplateRef<any>);
|
||||
private viewContainer = inject(ViewContainerRef);
|
||||
private authService = inject(Auth);
|
||||
private hasView = false;
|
||||
|
||||
permission = input.required<string | string[]>({ alias: "appHasPermission" });
|
||||
mode = input<"any" | "all">("any");
|
||||
permission = input.required<string | string[]>({ alias: 'appHasPermission' });
|
||||
mode = input<'any' | 'all'>('any');
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const hasPermission = this.checkPermission();
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const hasPermission = this.checkPermission();
|
||||
|
||||
if (hasPermission && !this.hasView) {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
this.hasView = true;
|
||||
} else if (!hasPermission && this.hasView) {
|
||||
this.viewContainer.clear();
|
||||
this.hasView = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkPermission(): boolean {
|
||||
const required = this.permission();
|
||||
const permissions = Array.isArray(required) ? required : [required];
|
||||
const userPermissions = this.authService.permissions();
|
||||
|
||||
if (this.mode() === "all") {
|
||||
return permissions.every((p) => userPermissions.includes(p));
|
||||
if (hasPermission && !this.hasView) {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
this.hasView = true;
|
||||
} else if (!hasPermission && this.hasView) {
|
||||
this.viewContainer.clear();
|
||||
this.hasView = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return permissions.some((p) => userPermissions.includes(p));
|
||||
}
|
||||
private checkPermission(): boolean {
|
||||
const required = this.permission();
|
||||
const permissions = Array.isArray(required) ? required : [required];
|
||||
const userPermissions = this.authService.permissions();
|
||||
|
||||
if (this.mode() === 'all') {
|
||||
return permissions.every((p) => userPermissions.includes(p));
|
||||
}
|
||||
|
||||
return permissions.some((p) => userPermissions.includes(p));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
@@ -553,23 +546,23 @@ export class HasPermission {
|
||||
|
||||
```typescript
|
||||
@Directive({
|
||||
selector: "[appToggle]",
|
||||
exportAs: "appToggle",
|
||||
selector: '[appToggle]',
|
||||
exportAs: 'appToggle',
|
||||
})
|
||||
export class Toggle {
|
||||
isOpen = signal(false);
|
||||
isOpen = signal(false);
|
||||
|
||||
toggle() {
|
||||
this.isOpen.update((v) => !v);
|
||||
}
|
||||
toggle() {
|
||||
this.isOpen.update((v) => !v);
|
||||
}
|
||||
|
||||
open() {
|
||||
this.isOpen.set(true);
|
||||
}
|
||||
open() {
|
||||
this.isOpen.set(true);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen.set(false);
|
||||
}
|
||||
close() {
|
||||
this.isOpen.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
|
||||
Reference in New Issue
Block a user