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

Create a new pull request by comparing changes across two branches #240

Merged
merged 10 commits into from
Oct 9, 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
2 changes: 1 addition & 1 deletion .github/actions/yarn-install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn'
runs:
using: 'composite'
steps:
- uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4
- uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4
with:
path: |
./node_modules/
Expand Down
3 changes: 2 additions & 1 deletion adev/shared-docs/styles/global-styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ $theme: mat.m2-define-light-theme(
@include mat.core();
@include mat.tabs-theme($theme);
@include mat.button-toggle-theme($theme);
@include mat.tooltip-theme($theme);

// Include custom docs styles
@include alert.docs-alert();
Expand Down Expand Up @@ -116,7 +117,7 @@ $theme: mat.m2-define-light-theme(
&.cli {
padding-inline-start: 1rem;
}

a {
color: inherit;
&:hover {
Expand Down
4 changes: 3 additions & 1 deletion adev/src/app/editor/code-editor/code-editor.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@
class="adev-editor-download-button"
type="button"
(click)="downloadCurrentCodeEditorState()"
aria-label="Download current code in editor"
aria-label="Download current source code"
matTooltip="Download current source code"
matTooltipPosition="above"
>
<docs-icon>download</docs-icon>
</button>
Expand Down
28 changes: 27 additions & 1 deletion adev/src/app/editor/code-editor/code-editor.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {CodeEditor, REQUIRED_FILES} from './code-editor.component';
import {CodeMirrorEditor} from './code-mirror-editor.service';
import {FakeChangeDetectorRef} from '@angular/docs';
import {TutorialType} from '@angular/docs';
import {MatTooltip} from '@angular/material/tooltip';
import {MatTooltipHarness} from '@angular/material/tooltip/testing';

const files = [
{filename: 'a', content: '', language: {} as any},
Expand Down Expand Up @@ -51,7 +53,7 @@ describe('CodeEditor', () => {

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CodeEditor, NoopAnimationsModule],
imports: [CodeEditor, NoopAnimationsModule, MatTooltip],
providers: [
{
provide: CodeMirrorEditor,
Expand Down Expand Up @@ -200,4 +202,28 @@ describe('CodeEditor', () => {
expect(fixture.debugElement.query(By.css('[aria-label="Delete file"]'))).toBeNull();
}
});

it('should be able to display the tooltip on the download button', async () => {
const tooltip = await loader.getHarness(
MatTooltipHarness.with({selector: '.adev-editor-download-button'}),
);
expect(await tooltip.isOpen()).toBeFalse();
await tooltip.show();
expect(await tooltip.isOpen()).toBeTrue();
});

it('should be able to get the tooltip message on the download button', async () => {
const tooltip = await loader.getHarness(
MatTooltipHarness.with({selector: '.adev-editor-download-button'}),
);
await tooltip.show();
expect(await tooltip.getTooltipText()).toBe('Download current source code');
});

it('should not be able to get the tooltip message on the download button when the tooltip is not shown', async () => {
const tooltip = await loader.getHarness(
MatTooltipHarness.with({selector: '.adev-editor-download-button'}),
);
expect(await tooltip.getTooltipText()).toBe('');
});
});
11 changes: 10 additions & 1 deletion adev/src/app/editor/code-editor/code-editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {StackBlitzOpener} from '../stackblitz-opener.service';
import {ClickOutside, IconComponent} from '@angular/docs';
import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu';
import {IDXLauncher} from '../idx-launcher.service';
import {MatTooltip} from '@angular/material/tooltip';

export const REQUIRED_FILES = new Set([
'src/main.ts',
Expand All @@ -48,7 +49,15 @@ const ANGULAR_DEV = 'https://angular.dev';
templateUrl: './code-editor.component.html',
styleUrls: ['./code-editor.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MatTabsModule, IconComponent, ClickOutside, CdkMenu, CdkMenuItem, CdkMenuTrigger],
imports: [
MatTabsModule,
MatTooltip,
IconComponent,
ClickOutside,
CdkMenu,
CdkMenuItem,
CdkMenuTrigger,
],
})
export class CodeEditor implements AfterViewInit, OnDestroy {
@ViewChild('codeEditorWrapper') private codeEditorWrapperRef!: ElementRef<HTMLDivElement>;
Expand Down
8 changes: 8 additions & 0 deletions adev/src/content/guide/di/dependency-injection-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ The following example defines a token, `APP_CONFIG`. of the type `InjectionToken
<docs-code header="src/app/app.config.ts" language="typescript" highlight="[3]">
import { InjectionToken } from '@angular/core';

export interface AppConfig {
title: string;
}

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config description');
</docs-code>

Expand All @@ -169,6 +173,10 @@ The optional type parameter, `<AppConfig>`, and the token description, `app.conf
Next, register the dependency provider in the component using the `InjectionToken` object of `APP_CONFIG`:

<docs-code header="src/app/app.component.ts" language="typescript">
const MY_APP_CONFIG_VARIABLE: AppConfig = {
title: 'Hello',
};

providers: [{ provide: APP_CONFIG, useValue: MY_APP_CONFIG_VARIABLE }]
</docs-code>

Expand Down
48 changes: 34 additions & 14 deletions devtools/projects/ng-devtools-backend/src/lib/component-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ const getDependenciesForDirective = (

let dependencies =
ngDebugClient().ɵgetDependenciesFromInjectable(injector, directive)?.dependencies ?? [];
const uniqueServices = new Set<string>();
const serializedInjectedServices: SerializedInjectedService[] = [];

let position = 0;
Expand Down Expand Up @@ -298,24 +299,43 @@ const getDependenciesForDirective = (
}),
];

if (dependency.token && isInjectionToken(dependency.token)) {
serializedInjectedServices.push({
token: dependency.token!.toString(),
let flags = dependency.flags as InjectOptions;
let flagToken = '';
if (flags !== undefined) {
// TODO: We need to remove this once the InjectFlags enum is removed from core
if (typeof flags === 'number') {
flags = {
optional: !!(flags & 8),
skipSelf: !!(flags & 4),
self: !!(flags & 2),
host: !!(flags & 1),
};
}
flagToken = (['optional', 'skipSelf', 'self', 'host'] as (keyof InjectOptions)[])
.filter((key) => flags[key])
.join('-');
}

const serviceKey = `${dependency.token}-${flagToken}`;
if (!uniqueServices.has(serviceKey)) {
uniqueServices.add(serviceKey);

const service = {
token: valueToLabel(dependency.token),
value: valueToLabel(dependency.value),
flags: dependency.flags as InjectOptions,
position: [position++],
flags,
position: [position],
resolutionPath: dependencyResolutionPath,
});
continue;
};

if (dependency.token && isInjectionToken(dependency.token)) {
service.token = dependency.token!.toString();
}

serializedInjectedServices.push(service);
}

serializedInjectedServices.push({
token: valueToLabel(dependency.token),
value: valueToLabel(dependency.value),
flags: dependency.flags as InjectOptions,
position: [position++],
resolutionPath: dependencyResolutionPath,
});
position++;
}

return serializedInjectedServices;
Expand Down
20 changes: 5 additions & 15 deletions packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2709,21 +2709,11 @@ class TcbExpressionTranslator {
* context). This method assists in resolving those.
*/
protected resolve(ast: AST): ts.Expression | null {
// TODO: this is actually a bug, because `ImplicitReceiver` extends `ThisReceiver`. Consider a
// case when the explicit `this` read is inside a template with a context that also provides the
// variable name being read:
// ```
// <ng-template let-a>{{this.a}}</ng-template>
// ```
// Clearly, `this.a` should refer to the class property `a`. However, because of this code,
// `this.a` will refer to `let-a` on the template context.
//
// Note that the generated code is actually consistent with this bug. To fix it, we have to:
// - Check `!(ast.receiver instanceof ThisReceiver)` in this condition
// - Update `ingest.ts` in the Template Pipeline (see the corresponding comment)
// - Turn off legacy TemplateDefinitionBuilder
// - Fix g3, and release in a major version
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) {
if (
ast instanceof PropertyRead &&
ast.receiver instanceof ImplicitReceiver &&
!(ast.receiver instanceof ThisReceiver)
) {
// Try to resolve a bound target for this expression. If no such target is available, then
// the expression is referencing the top-level component context. In that case, `null` is
// returned here to let it fall through resolution so it will be caught when the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1249,13 +1249,13 @@ import * as i0 from "@angular/core";
export class MyComponent {
}
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: '<ng-template let-a [ngIf]="true">{{this.a}}</ng-template>', isInline: true });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: '<ng-template let-a [ngIf]="true">{{a}}</ng-template>', isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,
args: [{
selector: 'my-component',
standalone: true,
template: '<ng-template let-a [ngIf]="true">{{this.a}}</ng-template>',
template: '<ng-template let-a [ngIf]="true">{{a}}</ng-template>',
}]
}] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ MyComponent_ng_template_0_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵtext(0);
} if (rf & 2) {
// NOTE: The fact that `this.` still refers to template context is a TDB and TCB bug; we should fix this eventually.
const $a_r1$ = ctx.$implicit;
i0.ɵɵtextInterpolate($a_r1$);
}
Expand All @@ -16,4 +15,4 @@ function MyComponent_Template(rf, ctx) {
} if (rf & 2) {
i0.ɵɵproperty("ngIf", true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Component} from '@angular/core';
@Component({
selector: 'my-component',
standalone: true,
template: '<ng-template let-a [ngIf]="true">{{this.a}}</ng-template>',
template: '<ng-template let-a [ngIf]="true">{{a}}</ng-template>',
})
export class MyComponent {
p1!: any;
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/render3/view/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,6 @@ export function compileComponentFromMetadata(
let hasStyles = !!meta.externalStyles?.length;
// e.g. `styles: [str1, str2]`
if (meta.styles && meta.styles.length) {
hasStyles = true;
const styleValues =
meta.encapsulation == core.ViewEncapsulation.Emulated
? compileStyles(meta.styles, CONTENT_ATTR, HOST_ATTR)
Expand All @@ -303,6 +302,7 @@ export function compileComponentFromMetadata(
}, [] as o.Expression[]);

if (styleNodes.length > 0) {
hasStyles = true;
definitionMap.set('styles', o.literalArr(styleNodes));
}
}
Expand Down
10 changes: 1 addition & 9 deletions packages/compiler/src/render3/view/t2_binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,21 +922,13 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
private maybeMap(ast: PropertyRead | SafePropertyRead | PropertyWrite, name: string): void {
// If the receiver of the expression isn't the `ImplicitReceiver`, this isn't the root of an
// `AST` expression that maps to a `Variable` or `Reference`.
if (!(ast.receiver instanceof ImplicitReceiver)) {
if (!(ast.receiver instanceof ImplicitReceiver) || ast.receiver instanceof ThisReceiver) {
return;
}

// Check whether the name exists in the current scope. If so, map it. Otherwise, the name is
// probably a property on the top-level component context.
const target = this.scope.lookup(name);

// It's not allowed to read template entities via `this`, however it previously worked by
// accident (see #55115). Since `@let` declarations are new, we can fix it from the beginning,
// whereas pre-existing template entities will be fixed in #55115.
if (target instanceof LetDeclaration && ast.receiver instanceof ThisReceiver) {
return;
}

if (target !== null) {
this.bindings.set(ast, target);
}
Expand Down
22 changes: 1 addition & 21 deletions packages/compiler/src/template/pipeline/src/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,30 +1003,10 @@ function convertAst(
if (ast instanceof e.ASTWithSource) {
return convertAst(ast.ast, job, baseSourceSpan);
} else if (ast instanceof e.PropertyRead) {
const isThisReceiver = ast.receiver instanceof e.ThisReceiver;
// Whether this is an implicit receiver, *excluding* explicit reads of `this`.
const isImplicitReceiver =
ast.receiver instanceof e.ImplicitReceiver && !(ast.receiver instanceof e.ThisReceiver);
// Whether the name of the read is a node that should be never retain its explicit this
// receiver.
const isSpecialNode = ast.name === '$any' || ast.name === '$event';
// TODO: The most sensible condition here would be simply `isImplicitReceiver`, to convert only
// actual implicit `this` reads, and not explicit ones. However, TemplateDefinitionBuilder (and
// the Typecheck block!) both have the same bug, in which they also consider explicit `this`
// reads to be implicit. This causes problems when the explicit `this` read is inside a
// template with a context that also provides the variable name being read:
// ```
// <ng-template let-a>{{this.a}}</ng-template>
// ```
// The whole point of the explicit `this` was to access the class property, but TDB and the
// current TCB treat the read as implicit, and give you the context property instead!
//
// For now, we emulate this old behavior by aggressively converting explicit reads to to
// implicit reads, except for the special cases that TDB and the current TCB protect. However,
// it would be an improvement to fix this.
//
// See also the corresponding comment for the TCB, in `type_check_block.ts`.
if (isImplicitReceiver || (isThisReceiver && !isSpecialNode)) {
if (isImplicitReceiver) {
return new ir.LexicalReadExpr(ast.name);
} else {
return new o.ReadPropExpr(
Expand Down
29 changes: 29 additions & 0 deletions packages/compiler/test/render3/view/binding_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,35 @@ describe('t2 binding', () => {
expect((target as a.LetDeclaration)?.name).toBe('value');
});

it('should not resolve a `this` access to a template reference', () => {
const template = parseTemplate(
`
<input #value>
{{this.value}}
`,
'',
);
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
const res = binder.bind({template: template.nodes});
const interpolationWrapper = (template.nodes[1] as a.BoundText).value as e.ASTWithSource;
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
const target = res.getExpressionTarget(propertyRead);

expect(target).toBe(null);
});

it('should not resolve a `this` access to a template variable', () => {
const template = parseTemplate(`<ng-template let-value>{{this.value}}</ng-template>`, '');
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
const res = binder.bind({template: template.nodes});
const templateNode = template.nodes[0] as a.Template;
const interpolationWrapper = (templateNode.children[0] as a.BoundText).value as e.ASTWithSource;
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
const target = res.getExpressionTarget(propertyRead);

expect(target).toBe(null);
});

it('should not resolve a `this` access to a `@let` declaration', () => {
const template = parseTemplate(
`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import assert from 'assert';
import ts from 'typescript';
import {isNodeDescendantOf} from './is_descendant_of';

/** Symbol that can be used to mark a variable as reserved, synthetically. */
export const ReservedMarker = Symbol();

// typescript/stable/src/compiler/types.ts;l=967;rcl=651008033
export interface LocalsContainer extends ts.Node {
locals?: Map<string, ts.Symbol>;
locals?: Map<string, ts.Symbol | typeof ReservedMarker>;
nextContainer?: LocalsContainer;
}

Expand Down Expand Up @@ -71,9 +74,9 @@ function isIdentifierFreeInContainer(name: string, container: LocalsContainer):
// Note: This check is similar to the check by the TypeScript emitter.
// typescript/stable/src/compiler/emitter.ts;l=5436;rcl=651008033
const local = container.locals.get(name)!;
return !(
local.flags &
(ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias)
return (
local !== ReservedMarker &&
!(local.flags & (ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias))
);
}

Expand Down
Loading
Loading