Skip to content

Commit 6fe9dfe

Browse files
feat(qwik-nx): preliminary implementation of angular integration (#197)
1 parent 71e06dc commit 6fe9dfe

File tree

18 files changed

+795
-2
lines changed

18 files changed

+795
-2
lines changed

packages/qwik-nx/generators.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@
8989
"factory": "./src/generators/integrations/deno/generator",
9090
"schema": "./src/generators/integrations/deno/schema.json",
9191
"description": "Qwik City Deno adaptor allows you to hook up Qwik City to a Deno server"
92+
},
93+
"angular-in-app": {
94+
"factory": "./src/generators/integrations/angular-in-app/generator",
95+
"schema": "./src/generators/integrations/angular-in-app/schema.json",
96+
"description": "angular-in-app generator",
97+
"hidden": true
9298
}
9399
}
94100
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import type { DocumentHead } from '@builder.io/qwik-city';
3+
import { AngularCounterComponent } from '../../integrations/angular';
4+
5+
export default component$(() => {
6+
return (
7+
<>
8+
<h1>Qwik/Angular demo</h1>
9+
<AngularCounterComponent initialCountValue={2} />
10+
</>
11+
);
12+
});
13+
14+
export const head: DocumentHead = {
15+
title: 'Qwik Angular',
16+
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { component$, useSignal } from '@builder.io/qwik';
2+
import type { DocumentHead } from '@builder.io/qwik-city';
3+
import {
4+
MaterialSlider,
5+
MaterialButton,
6+
type ButtonComponentProps,
7+
MaterialTable,
8+
type TableUserData,
9+
} from '../../integrations/angular';
10+
11+
export default component$(() => {
12+
const show = useSignal(false);
13+
const count = useSignal(0);
14+
const btnColor = useSignal<ButtonComponentProps['color']>('primary');
15+
const users = useSignal(Array.from({ length: 100 }, (_, k) => createNewUser(k + 1)));
16+
17+
return (
18+
<div>
19+
<h1>
20+
Welcome to Qwik Angular<span class="lightning">⚡️</span>
21+
</h1>
22+
23+
<div style="width: 80%; margin: 2rem auto">
24+
<select
25+
value={btnColor.value}
26+
onChange$={(ev) => {
27+
btnColor.value = (ev.target as any).value;
28+
}}
29+
>
30+
<option>warn</option>
31+
<option>accent</option>
32+
<option selected>primary</option>
33+
</select>
34+
35+
<MaterialSlider
36+
client:visible
37+
sliderValue={count.value}
38+
sliderValueChanged$={(value: number) => {
39+
count.value = value;
40+
}}
41+
/>
42+
43+
<MaterialButton color={btnColor.value} host:onClick$={() => alert('click')}>
44+
Slider is {count.value}
45+
</MaterialButton>
46+
47+
<MaterialButton
48+
color="accent"
49+
client:hover
50+
host:onClick$={() => {
51+
show.value = true;
52+
}}
53+
>
54+
Show table
55+
</MaterialButton>
56+
57+
{show.value && <MaterialTable client:only users={users.value}></MaterialTable>}
58+
</div>
59+
</div>
60+
);
61+
});
62+
63+
export const head: DocumentHead = {
64+
title: 'Qwik Angular',
65+
};
66+
67+
/** Builds and returns a new User. */
68+
function createNewUser(id: number): TableUserData {
69+
/** Constants used to fill up our data base. */
70+
const FRUITS: string[] = [
71+
'blueberry',
72+
'lychee',
73+
'kiwi',
74+
'mango',
75+
'peach',
76+
'lime',
77+
'pomegranate',
78+
'pineapple',
79+
];
80+
const NAMES: string[] = [
81+
'Maia',
82+
'Asher',
83+
'Olivia',
84+
'Atticus',
85+
'Amelia',
86+
'Jack',
87+
'Charlotte',
88+
'Theodore',
89+
'Isla',
90+
'Oliver',
91+
'Isabella',
92+
'Jasper',
93+
'Cora',
94+
'Levi',
95+
'Violet',
96+
'Arthur',
97+
'Mia',
98+
'Thomas',
99+
'Elizabeth',
100+
];
101+
const name =
102+
NAMES[Math.round(Math.random() * (NAMES.length - 1))] +
103+
' ' +
104+
NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) +
105+
'.';
106+
107+
return {
108+
id: id.toString(),
109+
name: name,
110+
progress: Math.round(Math.random() * 100).toString(),
111+
fruit: FRUITS[Math.round(Math.random() * (FRUITS.length - 1))],
112+
};
113+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
formatFiles,
3+
generateFiles,
4+
joinPathFragments,
5+
ProjectType,
6+
readProjectConfiguration,
7+
Tree,
8+
} from '@nx/devkit';
9+
import * as path from 'path';
10+
import { AngularInAppGeneratorSchema } from './schema';
11+
import { angularInit } from '../../../utils/angular/init';
12+
13+
interface NormalizedSchema extends AngularInAppGeneratorSchema {
14+
sourceRoot: string;
15+
projectRoot: string;
16+
projectType: ProjectType;
17+
}
18+
19+
function normalizeOptions(
20+
tree: Tree,
21+
options: AngularInAppGeneratorSchema
22+
): NormalizedSchema {
23+
const projectConfig = readProjectConfiguration(tree, options.project);
24+
25+
return {
26+
...options,
27+
installMaterialExample: options.installMaterialExample !== false,
28+
sourceRoot: projectConfig.sourceRoot ?? projectConfig.root + '/src',
29+
projectRoot: projectConfig.root,
30+
projectType: projectConfig.projectType!,
31+
};
32+
}
33+
34+
function addFiles(tree: Tree, normalizedOptions: NormalizedSchema): void {
35+
const filePath = normalizedOptions.installMaterialExample
36+
? 'material'
37+
: 'demo';
38+
generateFiles(
39+
tree,
40+
path.join(__dirname, 'files', filePath),
41+
joinPathFragments(normalizedOptions.sourceRoot, 'routes/angular'),
42+
{}
43+
);
44+
}
45+
46+
export async function angularInAppGenerator(
47+
tree: Tree,
48+
schema: AngularInAppGeneratorSchema
49+
) {
50+
const normalizedOptions = normalizeOptions(tree, schema);
51+
52+
if (normalizedOptions.projectType !== 'application') {
53+
throw new Error(
54+
`Only applications are supported, "${normalizedOptions.project}" is a library.`
55+
);
56+
}
57+
58+
const demoFilePath = joinPathFragments(
59+
normalizedOptions.sourceRoot,
60+
'integrations/angular'
61+
);
62+
63+
if (tree.exists(demoFilePath)) {
64+
throw new Error(
65+
`Looks like angular integration has already been configured for ${normalizedOptions.project}. "${demoFilePath}" already exists.`
66+
);
67+
}
68+
69+
const initCallback = angularInit(tree, {
70+
demoFilePath: joinPathFragments(
71+
normalizedOptions.sourceRoot,
72+
'integrations/angular'
73+
),
74+
installMaterialExample: !!normalizedOptions.installMaterialExample,
75+
projectRoot: normalizedOptions.projectRoot,
76+
isApp: true,
77+
});
78+
addFiles(tree, normalizedOptions);
79+
if (!normalizedOptions.skipFormat) {
80+
await formatFiles(tree);
81+
}
82+
83+
return initCallback;
84+
}
85+
86+
export default angularInAppGenerator;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface AngularInAppGeneratorSchema {
2+
project: string;
3+
installMaterialExample?: boolean;
4+
skipFormat?: boolean;
5+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"$id": "AngularInApp",
4+
"title": "",
5+
"type": "object",
6+
"properties": {
7+
"project": {
8+
"type": "string",
9+
"description": "Name of the project to add Angular integration to",
10+
"$default": {
11+
"$source": "argv",
12+
"index": 0
13+
},
14+
"x-prompt": "Name of the project to add Angular integration to"
15+
},
16+
"installMaterialExample": {
17+
"type": "boolean",
18+
"description": "Add dependencies for the Angular Material and qwikified example component, that uses it",
19+
"x-priority": "important",
20+
"default": true,
21+
"x-prompt": "Do you want to have Angular Material example installed?"
22+
},
23+
"skipFormat": {
24+
"description": "Skip formatting files.",
25+
"type": "boolean",
26+
"x-priority": "internal",
27+
"default": false
28+
}
29+
},
30+
"required": ["project"]
31+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Component, EventEmitter, Input, Output, type OnInit } from '@angular/core';
2+
import type { QwikifiedComponentProps, WithRequiredProps } from '@qwikdev/qwik-angular';
3+
4+
type CounterComponentInputs = 'initialCountValue' | 'heading';
5+
6+
type CounterComponentOutputs = 'countChanged';
7+
8+
type RequiredPropValues = 'initialCountValue';
9+
10+
// using utility types to assemble a type object for qwikified CounterComponent
11+
// that has all inputs and typed output handlers of Angular CounterComponent
12+
type OptionalCounterComponentProps = QwikifiedComponentProps<
13+
CounterComponent,
14+
CounterComponentInputs,
15+
CounterComponentOutputs
16+
>;
17+
18+
// also marking "initialCountValue" as required and exporting the final type
19+
export type CounterComponentProps = WithRequiredProps<
20+
OptionalCounterComponentProps,
21+
RequiredPropValues
22+
>;
23+
24+
@Component({
25+
selector: 'app-angular-counter',
26+
template: `
27+
<div class="wrapper">
28+
<h1>{{ heading }}</h1>
29+
<p>{{ count }}</p>
30+
<button (click)="handleClick()">Increment</button>
31+
</div>
32+
`,
33+
styles: [`.wrapper { display: flex; flex-direction: column; align-items: center; }`],
34+
standalone: true
35+
})
36+
export class CounterComponent implements OnInit {
37+
@Input() initialCountValue: number = 0;
38+
@Input() heading = 'Simple Angular Counter';
39+
40+
@Output() readonly countChanged = new EventEmitter<number>();
41+
42+
private count: number;
43+
44+
ngOnInit(): void {
45+
this.count = this.initialCountValue;
46+
}
47+
48+
handleClick(): void {
49+
this.count++;
50+
this.countChanged.emit(this.count);
51+
console.log(`Count: ${this.count}`);
52+
}
53+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { qwikify$ } from '@qwikdev/qwik-angular';
2+
import { type CounterComponentProps, CounterComponent } from './components/counter.component';
3+
4+
export const AngularCounterComponent = qwikify$<CounterComponentProps>(CounterComponent, {
5+
eagerness: 'hover',
6+
});
7+
8+
export { CounterComponentProps };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Component, Input } from '@angular/core';
2+
import { MatButtonModule } from '@angular/material/button';
3+
import type { QwikifiedComponentProps } from '@qwikdev/qwik-angular';
4+
5+
type ButtonComponentInputProps = 'color';
6+
7+
export type ButtonComponentProps = QwikifiedComponentProps<
8+
ButtonComponent,
9+
ButtonComponentInputProps
10+
>;
11+
12+
@Component({
13+
imports: [MatButtonModule],
14+
standalone: true,
15+
template: `
16+
<button mat-raised-button [color]="color">
17+
<ng-content></ng-content>
18+
</button>
19+
`,
20+
})
21+
export class ButtonComponent {
22+
@Input() color: 'primary' | 'accent' | 'warn' = 'primary';
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Component } from '@angular/core';
2+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
3+
import { MatFormFieldModule } from '@angular/material/form-field';
4+
import { MatInputModule } from '@angular/material/input';
5+
import { MatIconModule } from '@angular/material/icon';
6+
import { CommonModule } from '@angular/common';
7+
8+
@Component({
9+
selector: 'input-clearable-example',
10+
template: `
11+
<mat-form-field class="example-form-field">
12+
<mat-label>Clearable input</mat-label>
13+
<input matInput type="text" [(ngModel)]="value" />
14+
<button *ngIf="value" matSuffix mat-icon-button aria-label="Clear" (click)="value = ''">
15+
<mat-icon>close</mat-icon>
16+
</button>
17+
</mat-form-field>
18+
`,
19+
standalone: true,
20+
providers: [],
21+
imports: [
22+
MatFormFieldModule,
23+
MatInputModule,
24+
FormsModule,
25+
ReactiveFormsModule,
26+
MatIconModule,
27+
CommonModule,
28+
],
29+
})
30+
export class InputComponent {
31+
value = 'Clear me';
32+
}

0 commit comments

Comments
 (0)