Skip to content

Commit

Permalink
feat(qwik-nx): preliminary implementation of angular integration (#197)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitry-stepanenko authored Sep 17, 2023
1 parent 71e06dc commit 6fe9dfe
Show file tree
Hide file tree
Showing 18 changed files with 795 additions and 2 deletions.
6 changes: 6 additions & 0 deletions packages/qwik-nx/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
"factory": "./src/generators/integrations/deno/generator",
"schema": "./src/generators/integrations/deno/schema.json",
"description": "Qwik City Deno adaptor allows you to hook up Qwik City to a Deno server"
},
"angular-in-app": {
"factory": "./src/generators/integrations/angular-in-app/generator",
"schema": "./src/generators/integrations/angular-in-app/schema.json",
"description": "angular-in-app generator",
"hidden": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { component$ } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
import { AngularCounterComponent } from '../../integrations/angular';

export default component$(() => {
return (
<>
<h1>Qwik/Angular demo</h1>
<AngularCounterComponent initialCountValue={2} />
</>
);
});

export const head: DocumentHead = {
title: 'Qwik Angular',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { component$, useSignal } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
import {
MaterialSlider,
MaterialButton,
type ButtonComponentProps,
MaterialTable,
type TableUserData,
} from '../../integrations/angular';

export default component$(() => {
const show = useSignal(false);
const count = useSignal(0);
const btnColor = useSignal<ButtonComponentProps['color']>('primary');
const users = useSignal(Array.from({ length: 100 }, (_, k) => createNewUser(k + 1)));

return (
<div>
<h1>
Welcome to Qwik Angular<span class="lightning">⚡️</span>
</h1>

<div style="width: 80%; margin: 2rem auto">
<select
value={btnColor.value}
onChange$={(ev) => {
btnColor.value = (ev.target as any).value;
}}
>
<option>warn</option>
<option>accent</option>
<option selected>primary</option>
</select>

<MaterialSlider
client:visible
sliderValue={count.value}
sliderValueChanged$={(value: number) => {
count.value = value;
}}
/>

<MaterialButton color={btnColor.value} host:onClick$={() => alert('click')}>
Slider is {count.value}
</MaterialButton>

<MaterialButton
color="accent"
client:hover
host:onClick$={() => {
show.value = true;
}}
>
Show table
</MaterialButton>

{show.value && <MaterialTable client:only users={users.value}></MaterialTable>}
</div>
</div>
);
});

export const head: DocumentHead = {
title: 'Qwik Angular',
};

/** Builds and returns a new User. */
function createNewUser(id: number): TableUserData {
/** Constants used to fill up our data base. */
const FRUITS: string[] = [
'blueberry',
'lychee',
'kiwi',
'mango',
'peach',
'lime',
'pomegranate',
'pineapple',
];
const NAMES: string[] = [
'Maia',
'Asher',
'Olivia',
'Atticus',
'Amelia',
'Jack',
'Charlotte',
'Theodore',
'Isla',
'Oliver',
'Isabella',
'Jasper',
'Cora',
'Levi',
'Violet',
'Arthur',
'Mia',
'Thomas',
'Elizabeth',
];
const name =
NAMES[Math.round(Math.random() * (NAMES.length - 1))] +
' ' +
NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) +
'.';

return {
id: id.toString(),
name: name,
progress: Math.round(Math.random() * 100).toString(),
fruit: FRUITS[Math.round(Math.random() * (FRUITS.length - 1))],
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
formatFiles,
generateFiles,
joinPathFragments,
ProjectType,
readProjectConfiguration,
Tree,
} from '@nx/devkit';
import * as path from 'path';
import { AngularInAppGeneratorSchema } from './schema';
import { angularInit } from '../../../utils/angular/init';

interface NormalizedSchema extends AngularInAppGeneratorSchema {
sourceRoot: string;
projectRoot: string;
projectType: ProjectType;
}

function normalizeOptions(
tree: Tree,
options: AngularInAppGeneratorSchema
): NormalizedSchema {
const projectConfig = readProjectConfiguration(tree, options.project);

return {
...options,
installMaterialExample: options.installMaterialExample !== false,
sourceRoot: projectConfig.sourceRoot ?? projectConfig.root + '/src',
projectRoot: projectConfig.root,
projectType: projectConfig.projectType!,
};
}

function addFiles(tree: Tree, normalizedOptions: NormalizedSchema): void {
const filePath = normalizedOptions.installMaterialExample
? 'material'
: 'demo';
generateFiles(
tree,
path.join(__dirname, 'files', filePath),
joinPathFragments(normalizedOptions.sourceRoot, 'routes/angular'),
{}
);
}

export async function angularInAppGenerator(
tree: Tree,
schema: AngularInAppGeneratorSchema
) {
const normalizedOptions = normalizeOptions(tree, schema);

if (normalizedOptions.projectType !== 'application') {
throw new Error(
`Only applications are supported, "${normalizedOptions.project}" is a library.`
);
}

const demoFilePath = joinPathFragments(
normalizedOptions.sourceRoot,
'integrations/angular'
);

if (tree.exists(demoFilePath)) {
throw new Error(
`Looks like angular integration has already been configured for ${normalizedOptions.project}. "${demoFilePath}" already exists.`
);
}

const initCallback = angularInit(tree, {
demoFilePath: joinPathFragments(
normalizedOptions.sourceRoot,
'integrations/angular'
),
installMaterialExample: !!normalizedOptions.installMaterialExample,
projectRoot: normalizedOptions.projectRoot,
isApp: true,
});
addFiles(tree, normalizedOptions);
if (!normalizedOptions.skipFormat) {
await formatFiles(tree);
}

return initCallback;
}

export default angularInAppGenerator;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface AngularInAppGeneratorSchema {
project: string;
installMaterialExample?: boolean;
skipFormat?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "AngularInApp",
"title": "",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "Name of the project to add Angular integration to",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Name of the project to add Angular integration to"
},
"installMaterialExample": {
"type": "boolean",
"description": "Add dependencies for the Angular Material and qwikified example component, that uses it",
"x-priority": "important",
"default": true,
"x-prompt": "Do you want to have Angular Material example installed?"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"x-priority": "internal",
"default": false
}
},
"required": ["project"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Component, EventEmitter, Input, Output, type OnInit } from '@angular/core';
import type { QwikifiedComponentProps, WithRequiredProps } from '@qwikdev/qwik-angular';

type CounterComponentInputs = 'initialCountValue' | 'heading';

type CounterComponentOutputs = 'countChanged';

type RequiredPropValues = 'initialCountValue';

// using utility types to assemble a type object for qwikified CounterComponent
// that has all inputs and typed output handlers of Angular CounterComponent
type OptionalCounterComponentProps = QwikifiedComponentProps<
CounterComponent,
CounterComponentInputs,
CounterComponentOutputs
>;

// also marking "initialCountValue" as required and exporting the final type
export type CounterComponentProps = WithRequiredProps<
OptionalCounterComponentProps,
RequiredPropValues
>;

@Component({
selector: 'app-angular-counter',
template: `
<div class="wrapper">
<h1>{{ heading }}</h1>
<p>{{ count }}</p>
<button (click)="handleClick()">Increment</button>
</div>
`,
styles: [`.wrapper { display: flex; flex-direction: column; align-items: center; }`],
standalone: true
})
export class CounterComponent implements OnInit {
@Input() initialCountValue: number = 0;
@Input() heading = 'Simple Angular Counter';

@Output() readonly countChanged = new EventEmitter<number>();

private count: number;

ngOnInit(): void {
this.count = this.initialCountValue;
}

handleClick(): void {
this.count++;
this.countChanged.emit(this.count);
console.log(`Count: ${this.count}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { qwikify$ } from '@qwikdev/qwik-angular';
import { type CounterComponentProps, CounterComponent } from './components/counter.component';

export const AngularCounterComponent = qwikify$<CounterComponentProps>(CounterComponent, {
eagerness: 'hover',
});

export { CounterComponentProps };
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Component, Input } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import type { QwikifiedComponentProps } from '@qwikdev/qwik-angular';

type ButtonComponentInputProps = 'color';

export type ButtonComponentProps = QwikifiedComponentProps<
ButtonComponent,
ButtonComponentInputProps
>;

@Component({
imports: [MatButtonModule],
standalone: true,
template: `
<button mat-raised-button [color]="color">
<ng-content></ng-content>
</button>
`,
})
export class ButtonComponent {
@Input() color: 'primary' | 'accent' | 'warn' = 'primary';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Component } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { CommonModule } from '@angular/common';

@Component({
selector: 'input-clearable-example',
template: `
<mat-form-field class="example-form-field">
<mat-label>Clearable input</mat-label>
<input matInput type="text" [(ngModel)]="value" />
<button *ngIf="value" matSuffix mat-icon-button aria-label="Clear" (click)="value = ''">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
`,
standalone: true,
providers: [],
imports: [
MatFormFieldModule,
MatInputModule,
FormsModule,
ReactiveFormsModule,
MatIconModule,
CommonModule,
],
})
export class InputComponent {
value = 'Clear me';
}
Loading

0 comments on commit 6fe9dfe

Please sign in to comment.