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(vite-plugin-angular): support inputs and outputs for .ng format #848

Merged
merged 1 commit into from
Jan 12, 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
11 changes: 8 additions & 3 deletions apps/ng-app/src/app/app.component.ng
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

const counter = signal(1);
const doubled = computed(() => counter() * 2);
const text = computed(() => `The count from parent is: ${counter()}`);
const todo = signal(null);

const increment = () => {
Expand All @@ -32,6 +33,10 @@
counter.update((value) => value - 1);
}

function onClick(event: MouseEvent) {
console.log('the click from Hello', event);
}

effect(() => {
console.log('counter changed', counter());
});
Expand All @@ -50,7 +55,7 @@

<template>
@if (counter() > 5) {
<Hello />
<Hello [text]="text()" (clicked)="onClick($event)" />
<AnotherOne />
<app-hello-original />
}
Expand All @@ -68,11 +73,11 @@
<p>Loading todo...</p>
}

<br/>
<br />

<a routerLink="/">Home</a> | <a routerLink="/about">About</a>

<br/>
<br />

<router-outlet />
</template>
Expand Down
27 changes: 26 additions & 1 deletion apps/ng-app/src/app/hello.ng
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
<script lang="ts">
import { DestroyRef, inject } from '@angular/core';
import {
DestroyRef,
inject,
input,
EventEmitter,
effect,
} from '@angular/core';

const { foo: aliasFoo = 'tran', ...rest } = { foo: 'chau' };
const [a, b, , c = 5, ...restArray] = [1, 2, 3];

const text = input('');

effect(() => {
console.log('text changed', text());
});

const clicked = new EventEmitter<MouseEvent>();

inject(DestroyRef).onDestroy(() => {
console.log('hello destroyed');
});
Expand All @@ -15,4 +29,15 @@
<p>{{ b }}</p>
<p>{{ c }}</p>
<p>{{ aliasFoo }}</p>
<p>Text from input: {{ text() }}</p>
<button (click)="clicked.emit($event)">Emit The Click</button>
</template>

<style>
:host {
display: block;
padding: 1rem;
border: 1px dashed red;
border-radius: 0.5rem;
}
</style>
2 changes: 1 addition & 1 deletion apps/ng-app/src/app/hello.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Hello from './hello.ng';
template: `
<p>I'm a boring hello</p>
<p>Below me is a cool hello though</p>
<Hello />
<Hello text="this is from the boring HelloOriginal" />
`,
imports: [Hello],
})
Expand Down
34 changes: 17 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@
"apps/docs-app"
],
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/cdk": "^17.0.1",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/material": "^17.0.1",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/platform-server": "^17.0.0",
"@angular/router": "^17.0.0",
"@angular/animations": "^17.1.0-rc.0",
"@angular/cdk": "^17.1.0-rc.0",
"@angular/common": "^17.1.0-rc.0",
"@angular/compiler": "^17.1.0-rc.0",
"@angular/core": "^17.1.0-rc.0",
"@angular/forms": "^17.1.0-rc.0",
"@angular/material": "^17.1.0-rc.0",
"@angular/platform-browser": "^17.1.0-rc.0",
"@angular/platform-browser-dynamic": "^17.1.0-rc.0",
"@angular/platform-server": "^17.1.0-rc.0",
"@angular/router": "^17.1.0-rc.0",
"@astrojs/mdx": "^1.1.0",
"@astrojs/react": "^3.0.0",
"@babel/core": "^7.21.8",
Expand Down Expand Up @@ -82,15 +82,15 @@
"zone.js": "^0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.0",
"@angular-devkit/core": "^17.0.0",
"@angular-devkit/schematics": "^17.0.0",
"@angular-devkit/build-angular": "^17.1.0-rc.1",
"@angular-devkit/core": "^17.1.0-rc.1",
"@angular-devkit/schematics": "^17.1.0-rc.1",
"@angular-eslint/eslint-plugin": "16.0.1",
"@angular-eslint/eslint-plugin-template": "16.0.1",
"@angular-eslint/template-parser": "16.0.1",
"@angular/cli": "^17.0.0",
"@angular/compiler-cli": "^17.0.0",
"@angular/language-service": "^17.0.0",
"@angular/cli": "^17.1.0-rc.1",
"@angular/compiler-cli": "^17.1.0-rc.0",
"@angular/language-service": "^17.1.0-rc.0",
"@astrojs/markdown-component": "^1.0.5",
"@commitlint/cli": "^17.4.2",
"@commitlint/config-conventional": "^17.4.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

exports[`authoring ng file > should process component as ng file 1`] = `
"import { Component, ChangeDetectionStrategy } from '@angular/core';
import { signal } from "@angular/core";
import { signal, input } from "@angular/core";
import { Output } from "@angular/core";

@Component({
standalone: true,
Expand All @@ -22,21 +23,48 @@ export default class VirtualAnalogComponent {
setTimeout(() => {
test = 'test';
}, 1000)
const counter = (this.counter = signal(0));
const counter = this.counter
const [a, b, , c = 4] = [1, 2, 3];
this.a = a;
this.b = b;
this.c = c;
const inputWithDefault = this.inputWithDefault
const inputWithoutDefault = this.inputWithoutDefault
const inputWithAlias = this.inputWithAlias
const inputWithoutDefaultWithAlias = this.inputWithoutDefaultWithAlias
const inputWithTransform = this.inputWithTransform
const requiredInput = this.requiredInput
const requiredInputWithTransform = this.requiredInputWithTransform
const output = this.output
const outputWithType = this.outputWithType

Object.defineProperties(this, {
test: { get() { return test; } },
});
}

protected Math = Math;
protected counter = signal(0);
protected a;
protected b;
protected c;
protected inputWithDefault = input("");
protected inputWithoutDefault = input<string>();
protected inputWithAlias = input("", { alias: "theAlias" });
protected inputWithoutDefaultWithAlias = input<string | undefined>(undefined, {
alias: "theAlias",
});
protected inputWithTransform = input<unknown, boolean>("", {
transform: booleanAttribute,
});
protected requiredInput = input.required<string>();
protected requiredInputWithTransform = input.required<unknown, number>({
transform: (value) => numberAttribute(value, 10),
});
@Output()
protected output = new EventEmitter();
@Output()
protected outputWithType = new EventEmitter<string>();
}
"
`;
Expand All @@ -51,7 +79,7 @@ import { inject, ElementRef, afterNextRender } from "@angular/core";
})
export default class VirtualAnalogDirective {
constructor() {
const elRef = (this.elRef = inject(ElementRef));
const elRef = this.elRef
afterNextRender(() => {
elRef.nativeElement.focus();
});
Expand All @@ -63,6 +91,8 @@ export default class VirtualAnalogDirective {
});
}

protected elRef = inject(ElementRef);

ngOnInit() {
this.onInit();
}
Expand Down
18 changes: 17 additions & 1 deletion packages/vite-plugin-angular/src/lib/authoring/ng.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { compileNgFile } from './ng';

const COMPONENT_CONTENT = `
<script lang="ts">
import { signal } from '@angular/core';
import { signal, input } from '@angular/core';

defineMetadata({
exposes: [Math]
Expand All @@ -16,6 +16,22 @@ setTimeout(() => {

const counter = signal(0);
const [a, b, , c = 4] = [1, 2, 3];

const inputWithDefault = input(""); // InputSignal<string, string>
const inputWithoutDefault = input<string>(); // InputSignal<string | undefined, string | undefined>
const inputWithAlias = input("", { alias: "theAlias" }); // InputSignal<string, string>
const inputWithoutDefaultWithAlias = input<string | undefined>(undefined, {
alias: "theAlias",
}); // InputSignal<string | undefined, string | undefined>
const inputWithTransform = input<unknown, boolean>("", {
transform: booleanAttribute,
}); // InputSignal<unknown, boolean>
const requiredInput = input.required<string>(); // InputSignal<string, string>
const requiredInputWithTransform = input.required<unknown, number>({
transform: (value) => numberAttribute(value, 10),
});
const output = new EventEmitter();
const outputWithType = new EventEmitter<string>();
</script>

<template>
Expand Down
102 changes: 89 additions & 13 deletions packages/vite-plugin-angular/src/lib/authoring/ng.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import {
ArrowFunction,
CallExpression,
ClassDeclaration,
ConstructorDeclaration,
Expression,
FunctionDeclaration,
Identifier,
NewExpression,
Node,
ObjectLiteralExpression,
OptionalKind,
Project,
PropertyDeclarationStructure,
Scope,
StructureKind,
SyntaxKind,
Expand Down Expand Up @@ -214,18 +220,53 @@ function processNgScript(
const isFunctionInitializer = isFunction(initializer);

if (!isLet) {
/**
* normal property
* const variable = initializer;
* We'll create a class property with the same variable name
*/
addVariableToConstructor(
targetConstructor,
initializer.getText(),
name,
'const',
true
);
const ioStructure = getIOStructure(initializer);

if (ioStructure) {
// add structure to class
targetClass.addProperty({
...ioStructure,
name,
scope: Scope.Protected,
});

// assign constructor variable
targetConstructor.addStatements(`const ${name} = this.${name}`);

// import Output (if needed)
if (ioStructure.decorators) {
const hasOutputImport = targetSourceFile.getImportDeclaration(
(importDeclaration) =>
importDeclaration.getModuleSpecifierValue() ===
'@angular/core' &&
importDeclaration
.getNamedImports()
.some(
(importSpecifier) =>
importSpecifier.getName() === 'Output'
)
);
if (!hasOutputImport) {
targetSourceFile.addImportDeclaration({
namedImports: ['Output'],
moduleSpecifier: '@angular/core',
});
}
}
} else {
/**
* normal property
* const variable = initializer;
* We'll create a class property with the same variable name
*/
addVariableToConstructor(
targetConstructor,
initializer.getText(),
name,
'const',
true
);
}
} else {
/**
* let variable = initializer;
Expand Down Expand Up @@ -402,8 +443,43 @@ function addVariableToConstructor(
targetConstructor.addStatements((statement += ';'));
}

function isFunction(initializer: Node) {
function isFunction(
initializer: Node
): initializer is ArrowFunction | FunctionDeclaration {
return (
Node.isArrowFunction(initializer) || Node.isFunctionDeclaration(initializer)
);
}

function getIOStructure(
initializer: Node
): Omit<OptionalKind<PropertyDeclarationStructure>, 'name'> | null {
const callableExpression =
(Node.isCallExpression(initializer) || Node.isNewExpression(initializer)) &&
initializer;

if (!callableExpression) return null;

const [expression, initializerText] = [
callableExpression.getExpression(),
callableExpression.getText(),
];

if (initializerText.includes('new EventEmitter')) {
return {
initializer: initializerText,
decorators: [{ name: 'Output', arguments: [] }],
};
}

if (
(Node.isPropertyAccessExpression(expression) &&
expression.getText() === 'input.required') ||
Node.isIdentifier(expression) ||
expression.getText() === 'input'
) {
return { initializer: initializer.getText() };
}

return null;
}
Loading
Loading