Skip to content

Commit

Permalink
feat(vite-plugin-angular): support inputs and outputs for .ng format (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
nartc committed Jan 12, 2024
1 parent 7ac2d23 commit a6468db
Show file tree
Hide file tree
Showing 8 changed files with 2,530 additions and 1,268 deletions.
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

0 comments on commit a6468db

Please sign in to comment.