Skip to content

Commit

Permalink
[WNMGDS-3009][WNMGDS-3010] Add Angular example project and fix Angula…
Browse files Browse the repository at this point in the history
…r web component content issue (#3275)

* Add a basic Angular example that imports the design system styles

* Configure it to allow web components and add a couple simple ones

* Copy and paste the other wc examples without the scripts

* Update version after patch

* Add some debug logging

* Strip the example down to just a few components

These components show the current issue, which is that our custom elements don't have any initial `innerHTML`, which causes various symptoms. For the month picker, we don't get the default checked state or the disabled months. For the alert content, we don't get any of the alert content, but we get the header because it was defined with an attribute.

* I think this fixes it!

When Angular renders the web component at first, it doesn't have any innerHTML, but then it inserts the content later and comes in under the watchful eye of our MutationObserver. This might be a bad assumption, but the code I've added just now assumes the same kind of thing that we assumed before that if we're getting mutations to the root content, it means that we want to replace the previous content and re-render everything. Instead of the mutation kicking off the `renderPreactComponent` function and trying to read the `innerHTML` that isn't really settled with the new content, it will take the new content from the mutations and use that instead as the new content to render. For this to work, we're not taking into account previously defined template elements. I have to think about this a bit more, but I think after our changes to make the web components more stateful, we don't actually need to keep the old template around...that could be wrong. I need to think about it more.

* Get rid of debug logging

* A unit test in `define.test.tsx` caught an issue

It was the `correctly caches children when moved in the DOM` test, and it caught that when a component was moved in the DOM it ended up with nested templates. The fix for that is to bring back the old logic of only creating a template if one doesn't already exist WHEN we're not dealing with inner content mutations.

* Add some web component examples back in

* Revert "A unit test in `define.test.tsx` caught an issue"

This reverts commit 32237c1.

Apparently we can't both make this unit test happy and have working Angular examples. I'm going to have to think about this scenario some more. If I bring back the code that will look for previous templates when `addedNodes` doesn't exist, nested web components don't work right in the Angular examples. Try checking out 32237c1 and running the `examples/angular` project.

* Fix c578c7c (see note)

I think next I'll try actually removing the template if it is empty so even the empty template doesn't get duplicated inside the new template

* This is a better version of the previous fix

where we not only create a new template if the previous one was empty, but we remove the empty template before creating the new one so that it doesn't make its way into the content of the new one. Before this fix, the Angular-rendered alert example with a nested button would end up looking like this in the DOM:

```html
<ds-button>
  <button
    type="button"
    class="ds-c-button ds-c-button--alternate ds-c-button--solid"
  >
    <template><span></span></template>
    Break things
  </button>
  <template>
    <span>
      <template><span></span></template>
      Break things
    </span>
  </template>
</ds-button>
```

The above HTML has an empty content inside the actual button content, and the top-level template for the `ds-button` element has a another template inside of it! After this fix, it looks as we expect it:

```html
<ds-button>
  <button
    type="button"
    class="ds-c-button ds-c-button--alternate ds-c-button--solid"
  >
    Break things
  </button>
  <template><span>Break things</span></template>
</ds-button>
```

* Edited the comment to try to make it clearer

* Update our nvm node version for testing

* Fix bad ds version after bump

* Move the template content into a separate HTML file—thanks, Michael!

* Update lockfile (automatically removing bad entries)

* An ugly solution: polling for changes to the template

The way I got to this solution is that I observed that Angular was actually making changes to the web component's `template` element contents whenever Angular was re-rendering (probably because of [this line](https://github.com/CMSgov/design-system/blob/pwolfert/angular-example/packages/design-system/src/components/web-components/preactement/define.ts#L393)). The problem is that I can't use a MutationObserver on template content because it's only a document fragment and not part of the DOM. Polling for changes works, but I don't think this is a production-level solution.

* Remove the polling part of my last commit

I just wanted to get that code in version control, but I don't think this is the long-term solution

* Polling now working in the Angular wrapper instead of the design system

Ideally if we have to have an Angular wrapper, we could detect changes made by Angular and trigger a re-render, but nothing has seemed to work. This is the only thing so far that has worked.

* Looks like custom event binding works even without the wrapper

* Remove experimental solutions for ticket 3041

* Add new comment and remove old one

* Add a README for the project

* Add some more unit tests around this functionality

* Using `flatMap` is more succinct and readible here

* Get rid of debug logs
  • Loading branch information
pwolfert authored Nov 13, 2024
1 parent 0d62723 commit 2f157e9
Show file tree
Hide file tree
Showing 16 changed files with 4,341 additions and 123 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.19.0
18.20.4
3 changes: 3 additions & 0 deletions examples/angular/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.angular
dist
node_modules
21 changes: 21 additions & 0 deletions examples/angular/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Example: An Angular project with TypeScript

This shows the usage of CMS design system components in a TypeScript-enabled [Angular](https://angular.dev/) project.

In this project, the design system package is installed as an npm dependency. Refer to [install using npm](https://design.cms.gov/getting-started/for-developers/#option-1-install-using-npm) for instructions on how to install the design system as a dependency package.

## Getting started

1. Install packages at the root of this repository (see [root README](../../README.md))
1. Start the application: `yarn start`

_Note: Whenever changes have been made to the design system packages, you must **clear the local cache** for this application to receive those changes by deleting the hidden `examples/angular/.angular` directory._

## 🧞 Commands

All commands are run from the root of the project, from a terminal:

| Command | Action |
| :----------- | :------------------------------------------ |
| `yarn start` | Starts local dev server at `localhost:4200` |
| `yarn build` | Build your production site to `./dist/` |
60 changes: 60 additions & 0 deletions examples/angular/angular.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"cli": {
"analytics": "1e1de97b-a744-405a-8b5a-0397bb3d01ce"
},
"newProjectRoot": "projects",
"projects": {
"demo": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"configurations": {
"development": {
"extractLicenses": false,
"namedChunks": true,
"optimization": false,
"sourceMap": true
},
"production": {
"aot": true,
"extractLicenses": true,
"namedChunks": false,
"optimization": true,
"outputHashing": "all",
"sourceMap": false
}
},
"options": {
"assets": [],
"index": "src/index.html",
"browser": "src/main.ts",
"outputPath": "dist/demo",
"polyfills": ["zone.js"],
"scripts": [],
"styles": ["src/global_styles.css"],
"tsConfig": "tsconfig.app.json"
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"development": {
"buildTarget": "demo:build:development"
},
"production": {
"buildTarget": "demo:build:production"
}
},
"defaultConfiguration": "development"
}
},
"prefix": "app",
"projectType": "application",
"root": "",
"schematics": {},
"sourceRoot": "src"
}
},
"version": 1
}
29 changes: 29 additions & 0 deletions examples/angular/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "angular-example",
"private": true,
"version": "11.1.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build"
},
"dependencies": {
"@angular/animations": "^18.1.0",
"@angular/common": "^18.1.0",
"@angular/compiler": "^18.1.0",
"@angular/core": "^18.1.0",
"@angular/forms": "^18.1.0",
"@angular/platform-browser": "^18.1.0",
"@angular/router": "^18.1.0",
"@cmsgov/design-system": "11.1.0",
"rxjs": "^7.8.1",
"tslib": "^2.5.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.1.0",
"@angular/cli": "^18.1.0",
"@angular/compiler-cli": "^18.1.0",
"typescript": "~5.5.0"
}
}
2 changes: 2 additions & 0 deletions examples/angular/src/global_styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import '@cmsgov/design-system/css/index.css';
@import '@cmsgov/design-system/css/core-theme.css';
11 changes: 11 additions & 0 deletions examples/angular/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>My app</title>
<meta charset="UTF-8" />
<base href="/" />
</head>
<body>
<app-root>Loading...</app-root>
</body>
</html>
102 changes: 102 additions & 0 deletions examples/angular/src/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<div class="ds-content">
<ds-skip-nav href="#main"></ds-skip-nav>
<ds-usa-banner></ds-usa-banner>
<main id="main" class="ds-l-container">
<h1 class="ds-text-heading--4xl ds-u-margin-top--2">Web-component examples</h1>

<div class="ds-u-measure--base ds-u-padding-bottom--4">
<p>This is an example showing how to use our library of interactive web components.</p>

<ds-alert
heading="{{alertHeading}}"
[attr.variation]="alertVariation ?? null"
class="ds-u-margin-y--2"
#myAlert
>
In accordance with HIPAA, this application does not store any data. All data is stored
locally on your computer and is not transmitted to any external servers. The data you enter
into this application is not stored or shared with anyone.
</ds-alert>

<ds-button (click)="onButtonClick()" variation="solid" *ngIf="!hideButton"
>Click here</ds-button
>

<ds-accordion bordered="true" class="ds-u-margin-top--2">
<ds-accordion-item heading="First Amendment" default-open="true">
<p>
We the People of the United States, in Order to form a more perfect Union, establish
Justice, insure domestic Tranquility, provide for the common defence, promote the
general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do
ordain and establish this Constitution for the United States of America.
</p>
</ds-accordion-item>
<ds-accordion-item heading="Second Amendment">
<p>
A well regulated Militia, being necessary to the security of a free State, the right of
the people to keep and bear Arms, shall not be infringed.
</p>
</ds-accordion-item>
</ds-accordion>

<ds-month-picker
requirement-label="Required."
hint="Select many."
name="fooPicker"
label="Select any months you want!"
>
<input type="checkbox" value="10" checked />
<input type="checkbox" value="11" checked disabled />
<input type="checkbox" value="12" disabled />
</ds-month-picker>

<ds-spinner aria-valuetext="aria sets spinner label!"
>children don't set spinner label</ds-spinner
>

<ds-choice type="checkbox" label="I agree to the terms and conditions" name="agree">
<div slot="checked-children">
<ds-alert class="ds-u-margin-top--1">Cool, we hoped you'd check this box.</ds-alert>
</div>
</ds-choice>

<ds-choice-list type="radio" label="Choice list example" name="foo">
<ds-choice label="Choice without associated children" value="no children" />
<ds-choice
label="Checked children"
hint="Selecting this checkbox will reveal its associated children."
value="checked children"
>
<div slot="checked-children">
<div class="ds-c-choice__checkedChild">
<ds-alert heading="🫣 Tag! You're it!">
You can reveal content by applying <code>checked-children</code> to the
<code>slot</code> attribute of an HTML element. Do not forget to include a
<code>div</code> element with the class <code>ds-c-choice__checkedChild</code> to
whatever content you want to show/hide so it gets side border showing an association
with its choice parent.
</ds-alert>
</div>
</div>
</ds-choice>
<ds-choice
label="Unchecked children"
hint="Selecting this checkbox will hide its associated children."
value="unchecked children"
>
<div slot="unchecked-children">
<div class="ds-c-choice__checkedChild">
<ds-alert variation="warn" heading="I banish thee!">
You can hide content by applying <code>unchecked-children</code> to the
<code>slot</code> attribute of an HTML element. Do not forget to include a
<code>div</code> element with the class <code>ds-c-choice__checkedChild</code> to
whatever content you want to show/hide so it gets side border showing an association
with its choice parent.
</ds-alert>
</div>
</div>
</ds-choice>
</ds-choice-list>
</div>
</main>
</div>
31 changes: 31 additions & 0 deletions examples/angular/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NgIf } from '@angular/common';
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, ViewChild } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import '@cmsgov/design-system/web-components'; // TODO: Move to project.demo.architect.build.options.scripts

@Component({
selector: 'app-root',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
templateUrl: './main.html',
imports: [NgIf],
})
export class App {
name = 'Angular';

alertVariation: string | null = null;
alertHeading = 'Confidentiality and medical data sharing';
hideButton = false;
@ViewChild('myAlert') myAlert!: ElementRef;

onButtonClick() {
this.hideButton = true;
this.alertVariation = 'success';
this.alertHeading = 'You did it!';
// Temporary workaround for components not responding to dynamic content changes made
// by Angular templates
this.myAlert.nativeElement.innerHTML = 'You successfully clicked a button.';
}
}

bootstrapApplication(App);
10 changes: 10 additions & 0 deletions examples/angular/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
30 changes: 30 additions & 0 deletions examples/angular/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": ["ES2022", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`define() when run in the browser renders child HTML 1`] = `
<description-pair
term="supercalifragilisticexpialidocious"
>
<dt>
supercalifragilisticexpialidocious
</dt>
<dd>
Something to say when the
<a
href="#cat"
>
cat's got your tongue
</a>
</dd>
<template />
</description-pair>
`;
Loading

0 comments on commit 2f157e9

Please sign in to comment.