Skip to content

Commit 517bc57

Browse files
committed
Improve popover fallbacks, improve search in menu, create a new core utility for binding inputs with forms inside projections
1 parent b72fae2 commit 517bc57

File tree

14 files changed

+317
-32
lines changed

14 files changed

+317
-32
lines changed

projects/design-system/src/app/app.routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ export const routes: Routes = [
7777
path: 'form-fields',
7878
loadComponent: () => import('./ship/form-fields/form-fields'),
7979
},
80+
{
81+
path: 'form-fields-experimental',
82+
loadComponent: () => import('./ship/form-fields/examples/experimental-form-field/experimental-form-field'),
83+
},
8084
{
8185
path: 'sidenavs',
8286
loadComponent: () => import('./ship/sidenavs/sidenavs'),

projects/design-system/src/app/ship/dialogs/examples/basic-dynamic-dialog/basic-dynamic-dialog.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,9 @@ export class BasicDynamicDialog {
1515

1616
openDialog() {
1717
const dialogRef = this.#dialog.open(SimpleDialogContentComponent, {
18-
data: { message: 'Hello from parent!', yellow: false },
18+
data: { message: 'hllo', yellow: true, hello: true },
1919
class: this.type() ?? '',
2020
});
21-
22-
dialogRef.closed.subscribe((res) => {
23-
console.log('closed res', res);
24-
});
25-
26-
dialogRef.component.closed.subscribe((res) => {
27-
console.log('closed res', res);
28-
});
2921
}
3022
}
3123

@@ -37,6 +29,6 @@ export class BasicDynamicDialog {
3729
`,
3830
})
3931
class SimpleDialogContentComponent {
40-
data = input<{ message: string; yellow: boolean }>();
32+
data = input<{ message: string; yellow: boolean; hello: boolean }>();
4133
closed = output<string>();
4234
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<section>
2+
<sh-form-field-experimental>
3+
<label>ReactiveFormsField: {{ reactiveFormValue() }}</label>
4+
<input placeholder="Placeholder no label..." type="text" [formControl]="reactiveFormControl" />
5+
</sh-form-field-experimental>
6+
7+
<sh-form-field-experimental>
8+
<label>NgModel: {{ ngModelControl() }}</label>
9+
<input placeholder="Placeholder no label..." type="text" [(ngModel)]="ngModelControl" />
10+
</sh-form-field-experimental>
11+
</section>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@use 'helpers' as *;
2+
3+
:host {
4+
display: block;
5+
padding: p2r(40);
6+
}
7+
8+
section {
9+
display: flex;
10+
flex-direction: column;
11+
gap: p2r(20);
12+
}
13+
14+
sh-form-field-experimental {
15+
display: flex;
16+
flex-direction: column;
17+
gap: p2r(8);
18+
max-width: 300px;
19+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
2+
import { toSignal } from '@angular/core/rxjs-interop';
3+
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
4+
import { ShipFormFieldExperimental } from 'ship-ui';
5+
6+
@Component({
7+
selector: 'app-experimental-form-field',
8+
imports: [ShipFormFieldExperimental, FormsModule, ReactiveFormsModule],
9+
templateUrl: './experimental-form-field.html',
10+
styleUrl: './experimental-form-field.scss',
11+
changeDetection: ChangeDetectionStrategy.OnPush,
12+
})
13+
export default class ExperimentalFormField {
14+
reactiveFormControl = new FormControl('reactive hello');
15+
ngModelControl = signal('yellow');
16+
17+
reactiveFormValue = toSignal(this.reactiveFormControl.valueChanges);
18+
19+
ngOnInit() {
20+
setTimeout(() => {
21+
this.reactiveFormControl.setValue('123 reactive');
22+
this.ngModelControl.set('678 ngModel');
23+
}, 5000);
24+
}
25+
}

projects/design-system/src/app/ship/menus/examples/search-menu-example/search-menu-example.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<ng-container menu>
44
@for (item of filteredItems; track item.value) {
55
<button (click)="select(item)" [class.selected]="selected === item.value">
6+
<p>hello world</p>
67
{{ item.label }}
78
</button>
89
}

projects/ship-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@ship-ui/core",
33
"license": "MIT",
4-
"version": "0.16.2",
4+
"version": "0.16.3",
55
"peerDependencies": {
66
"@angular/common": ">=20",
77
"@angular/core": ">=20"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ChangeDetectionStrategy, Component, effect } from '@angular/core';
2+
import { createFormInputSignal } from '../utilities/create-form-input-signal';
3+
4+
@Component({
5+
selector: 'sh-form-field-experimental',
6+
imports: [],
7+
template: `
8+
<ng-content></ng-content>
9+
10+
<button (click)="myClick()">hello</button>
11+
`,
12+
changeDetection: ChangeDetectionStrategy.OnPush,
13+
})
14+
export class ShipFormFieldExperimental {
15+
firstInput = createFormInputSignal();
16+
17+
hello = effect(() => {
18+
console.log('hello', this.firstInput());
19+
});
20+
21+
myClick() {
22+
this.firstInput.update((x) => x + 'hello');
23+
}
24+
}

projects/ship-ui/src/lib/ship-menu/ship-menu.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { ShipFormField } from '../ship-form-field/ship-form-field';
1717
import { ShipIcon } from '../ship-icon/ship-icon';
1818
import { ShipPopover } from '../ship-popover/ship-popover';
19-
import { createInputSignal } from '../utilities/create-input-signal';
19+
import { createFormInputSignal } from '../utilities/create-form-input-signal';
2020
import { observeChildren } from '../utilities/observe-elements';
2121

2222
@Component({
@@ -82,8 +82,7 @@ export class ShipMenu {
8282

8383
options = observeChildren<HTMLButtonElement>(this.optionsRef, this.customOptionElementSelectors);
8484
optionsEl = computed(() => this.options.signal().filter((x) => !x.disabled));
85-
86-
inputValue = createInputSignal<string>(this.inputRef);
85+
inputValue = createFormInputSignal(this.inputRef);
8786

8887
abortController: AbortController | null = null;
8988
optionsEffect = effect(() => {
@@ -97,6 +96,7 @@ export class ShipMenu {
9796
this.abortController = new AbortController();
9897

9998
const searchable = this.searchable();
99+
100100
if (!searchable) {
101101
this.#document.documentElement.addEventListener('keydown', this.keyDownEventListener, {
102102
signal: this.abortController.signal,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import {
2+
DestroyRef,
3+
effect,
4+
ElementRef,
5+
inject,
6+
Injector,
7+
signal,
8+
type Signal,
9+
type WritableSignal,
10+
} from '@angular/core';
11+
import { observeFirstChild } from './observe-elements';
12+
13+
export function createFormInputSignal<T extends HTMLInputElement | HTMLTextAreaElement>(
14+
formEl?: Signal<ElementRef<T> | undefined>,
15+
elementTags: string[] = ['input', 'textarea']
16+
): WritableSignal<string | undefined> {
17+
const elementRefSignal = formEl ? formEl : observeFirstChild<T>(inject(ElementRef), elementTags);
18+
const valueSignal: WritableSignal<string | undefined> = signal(undefined);
19+
const injector = inject(Injector);
20+
const destroyRef = injector.get(DestroyRef);
21+
const firstInputEl = signal<HTMLInputElement | HTMLTextAreaElement | null>(null);
22+
23+
effect(
24+
(onCleanup) => {
25+
const elRef = elementRefSignal();
26+
27+
if (!elRef) return;
28+
29+
const inputEl = elRef.nativeElement;
30+
31+
if (!inputEl) return;
32+
33+
createCustomInputEventListener(inputEl);
34+
35+
firstInputEl.set(inputEl);
36+
valueSignal.set(inputEl.value ?? '');
37+
38+
onCleanup(() => {
39+
firstInputEl.set(null);
40+
});
41+
},
42+
{ injector }
43+
);
44+
45+
let removeListener: (() => void) | null = null;
46+
47+
effect(
48+
() => {
49+
const inputEl = firstInputEl();
50+
51+
if (!inputEl) return;
52+
53+
if (removeListener) {
54+
removeListener();
55+
removeListener = null;
56+
}
57+
58+
const inputChangedListener = (event: Event) => {
59+
valueSignal.set((event as CustomEvent<{ value: string }>).detail.value);
60+
};
61+
62+
const inputListener = (event: Event) => {
63+
valueSignal.set((event.target as HTMLInputElement | HTMLTextAreaElement).value);
64+
};
65+
66+
inputEl.addEventListener('input', inputListener as EventListener);
67+
inputEl.addEventListener('inputValueChanged', inputChangedListener as EventListener);
68+
69+
valueSignal.set(inputEl.value);
70+
71+
removeListener = () => {
72+
inputEl.removeEventListener('input', inputListener as EventListener);
73+
inputEl.removeEventListener('inputValueChanged', inputChangedListener as EventListener);
74+
};
75+
76+
destroyRef.onDestroy(() => {
77+
if (removeListener) removeListener();
78+
});
79+
},
80+
{ injector }
81+
);
82+
83+
effect(
84+
() => {
85+
const inputEl = firstInputEl();
86+
87+
if (!inputEl) return;
88+
89+
const inputValue = valueSignal();
90+
91+
if (!inputValue) return;
92+
93+
inputEl.value = inputValue;
94+
inputEl.dispatchEvent(new Event('input'));
95+
},
96+
{ injector }
97+
);
98+
99+
return valueSignal;
100+
}
101+
102+
function createCustomInputEventListener(input: HTMLInputElement | HTMLTextAreaElement) {
103+
Object.defineProperty(input, 'value', {
104+
configurable: true,
105+
get() {
106+
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), 'value');
107+
return descriptor!.get!.call(this);
108+
},
109+
set(newVal) {
110+
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), 'value');
111+
descriptor!.set!.call(this, newVal);
112+
113+
const inputEvent = new CustomEvent('inputValueChanged', {
114+
bubbles: true,
115+
cancelable: true,
116+
detail: {
117+
value: newVal,
118+
},
119+
});
120+
121+
this.dispatchEvent(inputEvent);
122+
123+
return newVal;
124+
},
125+
});
126+
127+
return input;
128+
}

0 commit comments

Comments
 (0)