diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f93e99efaf..085b4c91d9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,15 @@ jobs: run: yarn devtools:test - name: Test build run: yarn devtools:build:chrome + - name: Install Cypress + run: yarn global add cypress@14.4.1 + - name: Cypress run + uses: cypress-io/github-action@6c143abc292aa835d827652c2ea025d098311070 # v6.10.1 + with: + command: cypress run --project ./devtools/cypress + start: yarn bazel run //devtools/src:devserver + wait-on: 'http://localhost:4200' + wait-on-timeout: 300 test: runs-on: ubuntu-latest-4core diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 37de12e3783..fe60c649164 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -60,6 +60,15 @@ jobs: run: yarn devtools:test - name: Test build run: yarn devtools:build:chrome + - name: Install Cypress + run: yarn global add cypress@14.4.1 + - name: Cypress run + uses: cypress-io/github-action@6c143abc292aa835d827652c2ea025d098311070 # v6.10.1 + with: + command: cypress run --project ./devtools/cypress + start: yarn bazel run //devtools/src:devserver + wait-on: 'http://localhost:4200' + wait-on-timeout: 300 test: runs-on: ubuntu-latest-8core diff --git a/devtools/cypress/cypress.config.js b/devtools/cypress/cypress.config.js new file mode 100644 index 00000000000..706dc62d9fb --- /dev/null +++ b/devtools/cypress/cypress.config.js @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +module.exports = { + e2e: { + specPattern: 'integration/*.e2e.js', + supportFile: 'support/index.js', + baseUrl: 'http://localhost:4200', + }, +}; diff --git a/devtools/cypress/integration/base.e2e.js b/devtools/cypress/integration/base.e2e.js index 714f7bf4f88..c70b5610d45 100644 --- a/devtools/cypress/integration/base.e2e.js +++ b/devtools/cypress/integration/base.e2e.js @@ -6,15 +6,13 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('Testing the Todo app Demo', () => { beforeEach(() => { cy.visit('/'); }); it('should contain the todos application', () => { - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().contains('Todos'); getBody().contains('About'); getBody().contains('Clear completed'); @@ -23,15 +21,15 @@ describe('Testing the Todo app Demo', () => { }); it('should contain the "Components" tab', () => { - cy.contains('.mat-tab-links', 'Components'); + cy.contains('.devtools-nav', 'Components'); }); it('should contain the "Profiler" tab', () => { - cy.contains('.mat-tab-links', 'Profiler'); + cy.contains('.devtools-nav', 'Profiler'); }); it('should contain "app-root" and "app-todo-demo" in the component tree', () => { - cy.contains('.tree-node', 'app-root'); - cy.contains('.tree-node', 'app-todo-demo'); + cy.contains('ng-tree-node', 'app-root'); + cy.contains('ng-tree-node', 'app-todo-demo'); }); }); diff --git a/devtools/cypress/integration/comment-nodes.e2e.js b/devtools/cypress/integration/comment-nodes.e2e.js index 492118e1766..fff56712e91 100644 --- a/devtools/cypress/integration/comment-nodes.e2e.js +++ b/devtools/cypress/integration/comment-nodes.e2e.js @@ -8,7 +8,7 @@ function showComments() { cy.get('#nav-buttons > button:nth-child(2)').click(); - cy.get('#mat-slide-toggle-3 > label > div').click(); + cy.get('.cdk-overlay-container mat-slide-toggle label:contains("Show comment nodes")').click(); } describe('Comment nodes', () => { @@ -17,14 +17,14 @@ describe('Comment nodes', () => { }); it('should not find any comment nodes by default', () => { - const nodes = cy.$$('.tree-node:contains("#comment")'); + const nodes = cy.$$('ng-tree-node:contains("#comment")'); expect(nodes.length).to.eql(0); }); it('should find comment nodes when the setting is enabled', () => { showComments(); cy.get('.tree-wrapper') - .find('.tree-node:contains("#comment")') + .find('ng-tree-node:contains("#comment")') .its('length') .should('not.eq', 0); }); diff --git a/devtools/cypress/integration/elements.e2e.js b/devtools/cypress/integration/elements.e2e.js index 66d05401b33..711200894f2 100644 --- a/devtools/cypress/integration/elements.e2e.js +++ b/devtools/cypress/integration/elements.e2e.js @@ -12,6 +12,9 @@ describe('Angular Elements', () => { }); it('should recognize the zippy as an Angular Element', () => { - cy.get('.tree-wrapper').find('.tree-node:contains("app-zippy")').its('length').should('eq', 1); + cy.get('.tree-wrapper') + .find('ng-tree-node:contains("app-zippy")') + .its('length') + .should('eq', 1); }); }); diff --git a/devtools/cypress/integration/item-tracking.e2e.js b/devtools/cypress/integration/item-tracking.e2e.js index 99146a9d45b..36aca2fc639 100644 --- a/devtools/cypress/integration/item-tracking.e2e.js +++ b/devtools/cypress/integration/item-tracking.e2e.js @@ -6,31 +6,29 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('Tracking items from application to component tree', () => { beforeEach(() => { cy.visit('/'); }); it('should have only one todo item on start', () => { - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo').contains('Buy milk'); }); cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .its('length') .should('eq', 2); }); it('should be able to detect a new todo from user and add it to the tree', () => { - cy.enter('#sample-app') + cy.enterIframe('#sample-app') .then((getBody) => { getBody().find('input.new-todo').type('Buy cookies{enter}'); }) .then(() => { - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo').contains('Buy milk'); getBody().find('app-todo').contains('Build something fun!'); @@ -39,7 +37,7 @@ describe('Tracking items from application to component tree', () => { }); }); - cy.get('.tree-wrapper .tree-node:contains("app-todo[TooltipDirective]")').should( + cy.get('.tree-wrapper ng-tree-node:contains("app-todo[TooltipDirective]")').should( 'have.length', 3, ); diff --git a/devtools/cypress/integration/node-search.e2e.js b/devtools/cypress/integration/node-search.e2e.js index cf6f2802f40..f7ef07b32f6 100644 --- a/devtools/cypress/integration/node-search.e2e.js +++ b/devtools/cypress/integration/node-search.e2e.js @@ -11,7 +11,7 @@ function checkSearchedNodesLength(type, length) { } function inputSearchText(text) { - cy.get('.filter-input').type(text, {force: true}); + cy.get('ng-filter .filter-input').type(text, {force: true}); } function checkComponentName(name) { @@ -19,7 +19,7 @@ function checkComponentName(name) { } function checkEmptyNodes() { - cy.get('.tree-wrapper').find('.matched').should('not.exist'); + cy.get('.tree-wrapper').find('.matched-text').should('not.exist'); } function clickSearchArrows(upwards) { @@ -44,7 +44,7 @@ describe('Search items in component tree', () => { it('should highlight correct nodes when searching and clear out', () => { inputSearchText('todo'); - checkSearchedNodesLength('.matched', 4); + checkSearchedNodesLength('.matched-text', 4); // clear search input inputSearchText('{backspace}{backspace}{backspace}{backspace}'); @@ -53,22 +53,23 @@ describe('Search items in component tree', () => { it('should highlight correct nodes when searching and using arrow keys', () => { inputSearchText('todo'); - checkSearchedNodesLength('.matched', 4); + checkSearchedNodesLength('.matched-text', 4); + checkComponentName('app-todo-demo'); // press down arrow clickSearchArrows(false); checkSearchedNodesLength('.selected', 1); - checkComponentName('app-todo-demo'); + checkComponentName('app-todos'); - // press up arrow + // press down arrow clickSearchArrows(false); checkSearchedNodesLength('.selected', 1); - checkComponentName('app-todos'); + checkComponentName('app-todo'); // press up arrow clickSearchArrows(true); checkSearchedNodesLength('.selected', 1); - checkComponentName('app-todo-demo'); + checkComponentName('app-todos'); // clear search input inputSearchText('{backspace}{backspace}{backspace}{backspace}'); @@ -76,7 +77,7 @@ describe('Search items in component tree', () => { }); it('should select correct node on enter', () => { - inputSearchText('todos{enter}'); + inputSearchText('todo{enter}'); checkSearchedNodesLength('.selected', 1); // should show correct buttons in breadcrumbs @@ -91,7 +92,7 @@ describe('Search items in component tree', () => { checkComponentName('app-todos'); // should display correct title for properties panel - cy.get('ng-property-view-header').should('have.text', 'app-todos'); + cy.get('ng-property-view-header').should('contain.text', 'app-todos'); // should show correct component properties cy.get('ng-property-view').find('mat-tree-node'); diff --git a/devtools/cypress/integration/node-selection.e2e.js b/devtools/cypress/integration/node-selection.e2e.js index 6cdcf3cee13..e3c506e6ac6 100644 --- a/devtools/cypress/integration/node-selection.e2e.js +++ b/devtools/cypress/integration/node-selection.e2e.js @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('node selection', () => { beforeEach(() => { cy.visit('/'); @@ -15,49 +13,55 @@ describe('node selection', () => { describe('logic after change detection', () => { it('should deselect node if it is no longer on the page', () => { - cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist'); + cy.get('.tree-wrapper') + .get('ng-tree-node:contains("app-todo[TooltipDirective]")') + .should('not.have.class', 'selected'); cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); - cy.get('.tree-wrapper').find('.tree-node.selected').its('length').should('eq', 1); + cy.get('.tree-wrapper') + .get('ng-tree-node:contains("app-todo[TooltipDirective]")') + .should('have.class', 'selected'); - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('a:contains("About")').click(); }); - cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist'); + cy.get('.tree-wrapper') + .get('ng-tree-node:contains("app-todo[TooltipDirective]")') + .should('not.exist'); }); it('should reselect the previously selected node if it is still present', () => { - cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist'); + cy.get('.tree-wrapper').get('ng-tree-node.selected').should('not.exist'); - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('input.new-todo').type('Buy cookies{enter}'); }); cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .last() .click({force: true}); - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo:contains("Buy milk")').find('.destroy').click(); }); - cy.get('.tree-wrapper').find('.tree-node.selected').its('length').should('eq', 1); + cy.get('.tree-wrapper').find('ng-tree-node.selected').its('length').should('eq', 1); }); it('should select nodes with same name', () => { cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .last() .click({force: true}); @@ -74,7 +78,7 @@ describe('node selection', () => { describe('breadcrumb logic', () => { it('should overflow when breadcrumb list is long enough', () => { cy.get('.tree-wrapper') - .find('.tree-node:contains("div[TooltipDirective]")') + .find('ng-tree-node:contains("div[TooltipDirective]")') .last() .click({force: true}) .then(() => { @@ -90,7 +94,7 @@ describe('node selection', () => { it('should scroll right when right scroll button is clicked', () => { cy.get('.tree-wrapper') - .find('.tree-node:contains("div[TooltipDirective]")') + .find('ng-tree-node:contains("div[TooltipDirective]")') .last() .click({force: true}) .then(() => { @@ -106,7 +110,7 @@ describe('node selection', () => { cy.get('ng-breadcrumbs') .find('.scroll-button') .last() - .click() + .click({force: true}) .then(() => { expect(scrollLeft()).to.be.greaterThan(0); }); @@ -116,7 +120,7 @@ describe('node selection', () => { it('should scroll left when left scroll button is clicked', () => { cy.get('.tree-wrapper') - .find('.tree-node:contains("div[TooltipDirective]")') + .find('ng-tree-node:contains("div[TooltipDirective]")') .last() .click({force: true}) .then(() => { @@ -132,14 +136,14 @@ describe('node selection', () => { cy.get('ng-breadcrumbs') .find('.scroll-button') .last() - .click() + .click({force: true}) .then(() => { expect(scrollLeft()).to.be.greaterThan(0); cy.get('ng-breadcrumbs') .find('.scroll-button') .first() - .click() + .click({force: true}) .then(() => { expect(scrollLeft()).to.eql(0); }); diff --git a/devtools/cypress/integration/property-edit.e2e.js b/devtools/cypress/integration/property-edit.e2e.js index 363f992c117..b9537db8eff 100644 --- a/devtools/cypress/integration/property-edit.e2e.js +++ b/devtools/cypress/integration/property-edit.e2e.js @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('edit properties of directive in the property view tab', () => { beforeEach(() => { cy.visit('/'); @@ -17,13 +15,13 @@ describe('edit properties of directive in the property view tab', () => { beforeEach(() => { // select todo node in component tree cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); }); it('should be able to enable editMode', () => { - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo input.edit').should('not.be.visible'); }); @@ -36,61 +34,38 @@ describe('edit properties of directive in the property view tab', () => { .type('true') .type('{enter}'); - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo input.edit').should('be.visible'); }); }); + }); - describe('edit todo property', () => { - beforeEach(() => { - // expand todo state - cy.get('.explorer-panel:contains("app-todo")') - .find('ng-property-view mat-tree-node:contains("todo")') - .click(); + describe('edit title property', () => { + beforeEach(() => { + cy.get('.tree-wrapper') + .find('ng-tree-node:contains("app-todos")') + .first() + .click({force: true}); + }); + + it('should change title in app when edited', () => { + cy.enterIframe('#sample-app').then((getBody) => { + getBody().find('#demo-app-title').contains('Angular Todo'); }); - it('should change todo label in app when edited', () => { - // check initial todo label - cy.enter('#sample-app').then((getBody) => { - getBody().find('app-todo').contains('Buy milk').its('length').should('eq', 1); - }); + // find title variable and run through edit logic + cy.get('.explorer-panel:contains("app-todos")') + .find('ng-property-view mat-tree-node:contains("title")') + .find('ng-property-editor .editor') + .click() + .find('.editor-input') + .clear() + .type('Hello World') + .type('{enter}'); - // find label variable and run through edit logic - cy.get('.explorer-panel:contains("app-todo")') - .find('ng-property-view mat-tree-node:contains("label")') - .find('ng-property-editor .editor') - .click() - .find('.editor-input') - .clear() - .type('Buy cookies') - .type('{enter}'); - - // assert that the page has been updated - cy.enter('#sample-app').then((getBody) => { - getBody().find('app-todo').contains('Buy cookies').its('length').should('eq', 1); - }); - }); - - it('should change todo completed in app when edited', () => { - // check initial todo completed status - cy.enter('#sample-app').then((getBody) => { - getBody().find('app-todo li').not('.completed').its('length').should('eq', 2); - }); - - // find completed variable and run through edit logic - cy.get('.explorer-panel:contains("app-todo")') - .find('ng-property-view mat-tree-node:contains("completed")') - .find('ng-property-editor .editor') - .click() - .find('.editor-input') - .clear() - .type('true') - .type('{enter}'); - - // assert that the page has been updated - cy.enter('#sample-app').then((getBody) => { - getBody().find('app-todo li.completed').its('length').should('eq', 1); - }); + // assert that the page has been updated + cy.enterIframe('#sample-app').then((getBody) => { + getBody().find('#demo-app-title').contains('Hello World'); }); }); }); diff --git a/devtools/cypress/integration/property-update.e2e.js b/devtools/cypress/integration/property-update.e2e.js index 1e9051cd818..bf00d0162f8 100644 --- a/devtools/cypress/integration/property-update.e2e.js +++ b/devtools/cypress/integration/property-update.e2e.js @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('change of the state should reflect in property update', () => { beforeEach(() => { cy.visit('/'); @@ -15,13 +13,13 @@ describe('change of the state should reflect in property update', () => { it('should update the property value', () => { // Complete the todo - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('input[type="checkbox"].toggle').first().click(); }); // Select the todo item cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); diff --git a/devtools/cypress/integration/view-component-metadata.e2e.js b/devtools/cypress/integration/view-component-metadata.e2e.js index e61c2382f59..25b55dc7ec9 100644 --- a/devtools/cypress/integration/view-component-metadata.e2e.js +++ b/devtools/cypress/integration/view-component-metadata.e2e.js @@ -16,49 +16,49 @@ describe('Viewing component metadata', () => { cy.visit('/'); }); - describe('viewing TodoComponent', () => { + describe('viewing TodosComponent', () => { beforeEach(() => - prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-todo[TooltipDirective]")'), + prepareHeaderExpansionPanelForAssertions('ng-tree-node:contains("app-todos")'), ); it('should display view encapsulation', () => { - cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: Emulated'); + cy.contains('.meta-data-container', 'View Encapsulation: None'); }); it('should display change detection strategy', () => { - cy.contains('.meta-data-container .mat-button:last', 'Change Detection Strategy: OnPush'); + cy.contains('.meta-data-container', 'Change Detection Strategy: Default'); }); }); describe('viewing DemoAppComponent', () => { beforeEach(() => - prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-demo-component")'), + prepareHeaderExpansionPanelForAssertions('ng-tree-node:contains("app-demo-component")'), ); it('should display view encapsulation', () => { - cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: None'); + cy.contains('.meta-data-container', 'View Encapsulation: None'); }); it('should display change detection strategy', () => { - cy.contains('.meta-data-container .mat-button:last', 'Change Detection Strategy: Default'); + cy.contains('.meta-data-container', 'Change Detection Strategy: Default'); }); it('should display correct set of inputs', () => { - cy.contains('.cy-inputs', 'Inputs'); - cy.contains('.cy-inputs mat-tree-node:first span:first', 'inputOne'); - cy.contains('.cy-inputs mat-tree-node:last span:first', 'inputTwo'); + cy.contains('.mat-accordion-content#Inputs', 'Inputs'); + cy.contains('.mat-accordion-content#Inputs mat-tree-node:first span:first', 'inputOne'); + cy.contains('.mat-accordion-content#Inputs mat-tree-node:last span:first', 'inputTwo'); }); it('should display correct set of outputs', () => { - cy.contains('.cy-outputs', 'Outputs'); - cy.contains('.cy-outputs mat-tree-node:first span:first', 'outputOne'); - cy.contains('.cy-outputs mat-tree-node:last span:first', 'outputTwo'); + cy.contains('.mat-accordion-content#Outputs', 'Outputs'); + cy.contains('.mat-accordion-content#Outputs mat-tree-node:first span:first', 'outputOne'); + cy.contains('.mat-accordion-content#Outputs mat-tree-node:last span:first', 'outputTwo'); }); it('should display correct set of properties', () => { - cy.contains('.cy-properties', 'Properties'); - cy.contains('.cy-properties mat-tree-node:first span:first', 'elementRef'); - cy.contains('.cy-properties mat-tree-node:last span:first', 'zippy'); + cy.contains('.mat-accordion-content#Properties', 'Properties'); + cy.contains('.mat-accordion-content#Properties mat-tree-node:first span:first', 'elementRef'); + cy.contains('.mat-accordion-content#Properties mat-tree-node:last span:first', 'zippy'); }); }); }); diff --git a/devtools/cypress/support/commands.js b/devtools/cypress/support/commands.js index 4ba9df19a44..e144a5c5dab 100644 --- a/devtools/cypress/support/commands.js +++ b/devtools/cypress/support/commands.js @@ -6,28 +6,25 @@ * found in the LICENSE file at https://angular.dev/license */ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +/** + * Selects an Iframe and returns its body when it's done loading. + * @param {string} selector - The selector for the iframe. + * @returns {Function} A function that returns the wrapped body of the iframe. + */ +function enterIframe(selector) { + return cy.get(selector, {log: false}).then({timeout: 30000}, async (frame) => { + const contentWindow = frame.prop('contentWindow'); + + while ( + contentWindow.location.toString() === 'about:blank' || + contentWindow.document.readyState !== 'complete' + ) { + await new Promise((resolve) => setTimeout(resolve)); + } + + // return the body of the iframe wrapped in cypress + return () => cy.wrap(contentWindow.document.body); + }); +} + +Cypress.Commands.add('enterIframe', enterIframe); diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html index 4ca38e274f3..818af18b262 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html @@ -23,7 +23,7 @@ } @for (panel of panels(); track $index) { -
+
@if (panel.controls().dataSource.data.length > 0) { diff --git a/devtools/src/app/demo-app/demo-app.component.html b/devtools/src/app/demo-app/demo-app.component.html index 9022b548952..6499a50d759 100644 --- a/devtools/src/app/demo-app/demo-app.component.html +++ b/devtools/src/app/demo-app/demo-app.component.html @@ -2,4 +2,5 @@ This is my content
HTMLElement
+
Test Structural Directive
diff --git a/devtools/src/app/demo-app/demo-app.component.ts b/devtools/src/app/demo-app/demo-app.component.ts index 19164b6693c..0f80c8e399c 100644 --- a/devtools/src/app/demo-app/demo-app.component.ts +++ b/devtools/src/app/demo-app/demo-app.component.ts @@ -10,11 +10,15 @@ import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, + Directive, ElementRef, + inject, input, output, signal, + TemplateRef, viewChild, + ViewContainerRef, ViewEncapsulation, } from '@angular/core'; @@ -23,13 +27,30 @@ import {HeavyComponent} from './heavy.component'; import {SamplePropertiesComponent} from './sample-properties.component'; import {RouterOutlet} from '@angular/router'; +// structual directive example +@Directive({ + selector: '[appStructural]', + host: { + '[class.app-structural]': 'true', + }, +}) +export class StructuralDirective { + templateRef = inject(TemplateRef); + viewContainerRef = inject(ViewContainerRef); + + ngOnInit() { + // Example of using the structural directive + this.viewContainerRef.createEmbeddedView(this.templateRef); + } +} + @Component({ selector: 'app-demo-component', templateUrl: './demo-app.component.html', styleUrls: ['./demo-app.component.scss'], encapsulation: ViewEncapsulation.None, schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [HeavyComponent, SamplePropertiesComponent, RouterOutlet], + imports: [StructuralDirective, HeavyComponent, SamplePropertiesComponent, RouterOutlet], }) export class DemoAppComponent { readonly zippy = viewChild(ZippyComponent); diff --git a/devtools/src/app/demo-app/todo/home/todos.component.html b/devtools/src/app/demo-app/todo/home/todos.component.html index d972095ea5b..5917c5c1031 100644 --- a/devtools/src/app/demo-app/todo/home/todos.component.html +++ b/devtools/src/app/demo-app/todo/home/todos.component.html @@ -4,7 +4,7 @@

{{ 'Sample text processed by a pipe' | sample }}

-

todos

+

{{title}}

{ imports: [RouterLink, TodoComponent, TooltipDirective, SamplePipe, TodosFilter], }) export class TodosComponent implements OnInit, OnDestroy { + title = 'Angular Todo'; + todos: Todo[] = [ { label: 'Buy milk',