Skip to content

Commit

Permalink
feat(docs-infra): add support for "special elements" (#41299)
Browse files Browse the repository at this point in the history
This commit adds support for generating pages that document
special Angular elements, such as `ng-content` and `ng-template`,
which have special behavior in Angular but are not directives nor
components.

Resolves #41273

PR Close #41299
  • Loading branch information
petebacondarwin authored and alxhub committed Jun 16, 2021
1 parent 6bf4cb6 commit 9c9ae27
Show file tree
Hide file tree
Showing 19 changed files with 201 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .pullapprove.yml
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,8 @@ groups:
'aio/content/images/guide/user-input/**',
'aio/content/guide/view-encapsulation.md',
'aio/content/examples/view-encapsulation/**',
'aio/content/images/guide/view-encapsulation/**'
'aio/content/images/guide/view-encapsulation/**',
'aio/content/special-elements/**'
])
reviewers:
users:
Expand Down
28 changes: 28 additions & 0 deletions aio/content/special-elements/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Special Elements

Each sub-directory below this contains documentation that describes "special elements".
These are elements that can appear in templates that have special meaning and behaviour in the Angular framework.

Each element should have a markdown file with the same file name as the element's tag name, e.g. `ng-container.md`.
The file should be stored in a directory whose name is that of the Angular package under which this element should appear in the docs (usually `core`).

## Short description

The file should contain a "short description" of the element. This is the first paragraph in the file.

## Long description

All the paragraphs after the short description are collected as an additional longer description.

## Element attributes

If the special element accepts one or more attributes that have special meaning to Angular, then these should be documented using the `@elementAttribute` tag.
These tags should come after the description.

The format of this tag is:

```
@elementAttribute attr="value"
Description of the attribute and value.
```
1 change: 1 addition & 0 deletions aio/content/special-elements/core/ng-container.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The `<ng-container>` can be used to hold directives without creating an HTML element.
5 changes: 5 additions & 0 deletions aio/content/special-elements/core/ng-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The `<ng-content>` element specifies where to project content inside a component template.

@elementAttribute select="selector"

Only select elements from the projected content that match the given CSS `selector`.
3 changes: 3 additions & 0 deletions aio/content/special-elements/core/ng-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Angular's `<ng-template>` element defines a template that doesn't render anything by default.

With `<ng-template>`, you can render the content manually for full control over how the content displays.
10 changes: 5 additions & 5 deletions aio/firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@

// URLs that use the old scheme of adding the type to the end (e.g. `SomeClass-class`)
// (Exclude disambiguated URLs that might be suffixed with `-\d+` (e.g. `SomeClass-1`))
{"type": 301, "regex": "^/api/(?P<package>[^/]+)/(?P<api>[^/]+)-\\D*$", "destination": "/api/:package/:api"},
{"type": 301, "regex": "^/api/(?P<package>[^/]+)/testing/index/(?P<api>[^/]+)$", "destination": "/api/:package/testing/:api"},
{"type": 301, "regex": "^/api/(?P<package>[^/]+)/testing/(?P<api>[^/]+)-\\D*$", "destination": "/api/:package/testing/:api"},
{"type": 301, "regex": "^/api/upgrade/(?P<package>[^/]+)/index/(?P<api>[^/]+)$", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "regex": "^/api/upgrade/(?P<package>[^/]+)/(?P<api>[^/]+)-\\D*$", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/api/:package/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/:package/:api"},
{"type": 301, "source": "/api/:package/testing/index/:api", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/api/:package/testing/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/api/upgrade/:package/index/:api", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/api/upgrade/:package/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/upgrade/:package/:api"},

// URLs that use the old scheme before we moved the docs to the angular/angular repo
{"type": 301, "source": "/docs/*/latest", "destination": "/docs"},
Expand Down
2 changes: 1 addition & 1 deletion aio/ngsw-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"!/**/*__*/**",
"!/**/stackblitz",
"!/**/stackblitz.html",
"!/api/*/**/*-\\D{0,}",
"!/api/*/**/*-(class|directive|var|interface|function|pipe|let|type-alias|decorator)",
"!/api/**/AnimationStateDeclarationMetadata*",
"!/api/**/CORE_DIRECTIVES*",
"!/api/**/DirectiveMetadata*",
Expand Down
3 changes: 2 additions & 1 deletion aio/src/app/custom-elements/api/api-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ export class ApiListComponent implements OnInit {
{ value: 'const', title: 'Const'},
{ value: 'decorator', title: 'Decorator' },
{ value: 'directive', title: 'Directive' },
{ value: 'element', title: 'Element'},
{ value: 'enum', title: 'Enum' },
{ value: 'function', title: 'Function' },
{ value: 'interface', title: 'Interface' },
{ value: 'package', title: 'Package'},
{ value: 'pipe', title: 'Pipe'},
{ value: 'ngmodule', title: 'NgModule'},
{ value: 'type-alias', title: 'Type alias' },
{ value: 'package', title: 'Package'}
];

statuses: Option[] = [
Expand Down
5 changes: 5 additions & 0 deletions aio/src/styles/_constants.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ $darkorange: #940;
$anti-pattern: $brightred;

// API & CODE COLORS
$amber-200: #AA3000;
$amber-700: #FFA000;
$blue-400: #42A5F5;
$blue-500: #2196F3;
Expand Down Expand Up @@ -121,6 +122,10 @@ $api-symbols: (
content: 'P',
background: $blue-grey-600
),
element: (
content: 'El',
background: $amber-200
),
type-alias: (
content: 'T',
background: $light-green-600
Expand Down
50 changes: 37 additions & 13 deletions aio/tools/transforms/angular-api-package/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const Package = require('dgeni').Package;

const basePackage = require('../angular-base-package');
const typeScriptPackage = require('dgeni-packages/typescript');
const {API_SOURCE_PATH, API_TEMPLATES_PATH, requireFolder} = require('../config');
const {API_SOURCE_PATH, API_TEMPLATES_PATH, requireFolder, CONTENTS_PATH} = require('../config');
const API_SEGMENT = 'api';

module.exports =
new Package('angular-api', [basePackage, typeScriptPackage])
Expand Down Expand Up @@ -37,6 +38,7 @@ module.exports =
.processor(require('./processors/simplifyMemberAnchors'))
.processor(require('./processors/computeStability'))
.processor(require('./processors/removeInjectableConstructors'))
.processor(require('./processors/processSpecialElements'))
.processor(require('./processors/collectPackageContentDocs'))
.processor(require('./processors/processPackages'))
.processor(require('./processors/processNgModuleDocs'))
Expand All @@ -50,7 +52,7 @@ module.exports =
* more Angular specific API types, such as decorators and directives.
*/
.factory(function API_DOC_TYPES_TO_RENDER(EXPORT_DOC_TYPES) {
return EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe', 'package']);
return EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe', 'package', 'element']);
})

/**
Expand All @@ -72,11 +74,12 @@ module.exports =
})

.factory(require('./readers/package-content'))
.factory(require('./readers/element'))

// Where do we get the source files?
.config(function(
readTypeScriptModules, readFilesProcessor, collectExamples, tsParser,
packageContentFileReader) {
packageContentFileReader, specialElementFileReader) {
// Tell TypeScript how to load modules that start with with `@angular`
tsParser.options.paths = {'@angular/*': [API_SOURCE_PATH + '/*']};
tsParser.options.baseUrl = '.';
Expand Down Expand Up @@ -122,24 +125,47 @@ module.exports =
'upgrade/static/testing/index.ts',
];

readFilesProcessor.fileReaders.push(packageContentFileReader);

// API Examples
// Special elements and packages docs are not extracted directly from TS code.
readFilesProcessor.fileReaders.push(packageContentFileReader, specialElementFileReader);
readFilesProcessor.sourceFiles = [
{
basePath: API_SOURCE_PATH,
include: API_SOURCE_PATH + '/examples/**/*',
fileReader: 'exampleFileReader'
include: API_SOURCE_PATH + '/**/PACKAGE.md',
fileReader: 'packageContentFileReader'
},
{
basePath: CONTENTS_PATH + '/special-elements',
include: CONTENTS_PATH + '/special-elements/*/**/*.md',
fileReader: 'specialElementFileReader'
},
{
basePath: API_SOURCE_PATH,
include: API_SOURCE_PATH + '/**/PACKAGE.md',
fileReader: 'packageContentFileReader'
include: API_SOURCE_PATH + '/examples/**/*',
fileReader: 'exampleFileReader'
}
];

collectExamples.exampleFolders.push('examples');
})

// Configure element ids and paths
.config(function(computeIdsProcessor, computePathsProcessor) {
computeIdsProcessor.idTemplates.push({
docTypes: ['element'],
getId(doc) {
// path should not have a suffix
return doc.fileInfo.relativePath.replace(/\.\w*$/, '');
},
getAliases(doc) { return [doc.name, doc.id]; }
});

computePathsProcessor.pathTemplates.push({
docTypes: ['element'],
pathTemplate: API_SEGMENT + '/${id}',
outputPathTemplate: '${path}.json'
});
})

// Configure jsdoc-style tag parsing
.config(function(parseTagsProcessor, getInjectables, tsHost) {
// Load up all the tag definitions in the tag-defs folder
Expand All @@ -154,7 +180,7 @@ module.exports =
API_DOC_TYPES_TO_RENDER, API_DOC_TYPES) {
computeStability.docTypes = API_DOC_TYPES_TO_RENDER;
// Only split the description on the API docs
splitDescription.docTypes = API_DOC_TYPES.concat(['package-content']);
splitDescription.docTypes = API_DOC_TYPES.concat(['package-content', 'element']);
addNotYetDocumentedProperty.docTypes = API_DOC_TYPES;
})

Expand Down Expand Up @@ -186,8 +212,6 @@ module.exports =


.config(function(computePathsProcessor, EXPORT_DOC_TYPES, generateApiListDoc) {
const API_SEGMENT = 'api';

generateApiListDoc.outputFolder = API_SEGMENT;

computePathsProcessor.pathTemplates.push({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = function processPackages(collectPackageContentDocsProcessor) {
doc.directives = publicExports.filter(doc => doc.docType === 'directive').sort(byId);
doc.pipes = publicExports.filter(doc => doc.docType === 'pipe').sort(byId);
doc.types = publicExports.filter(doc => doc.docType === 'type-alias' || doc.docType === 'const').sort(byId);
doc.elements = publicExports.filter(doc => doc.docType === 'element').sort(byId);
if (doc.hasPublicExports && publicExports.every(doc => !!doc.deprecated)) {
doc.deprecated = 'all exports of this entry point are deprecated.';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const path = require('canonical-path');

module.exports = function processSpecialElements() {
return {
$runAfter: ['tags-extracted'],
$runBefore: ['collectPackageContentDocsProcessor'],
$process(docs) {
const moduleDocs = {};
docs.forEach(doc => {
if (doc.docType === 'module') {
moduleDocs[doc.id] = doc;
}
});

docs.forEach(doc => {
// Wire up each 'element' doc to its containing module/package.
if (doc.docType === 'element') {
doc.moduleDoc = moduleDocs[path.dirname(doc.fileInfo.relativePath)];
doc.moduleDoc.exports.push(doc);
}
});
}
};
};
29 changes: 29 additions & 0 deletions aio/tools/transforms/angular-api-package/readers/element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @dgService
* @description
* This file reader will pull the contents from a text file that will be used
* as the description of a "special element", such as `<ng-content>` or `<ng-template>`, etc.
*
* The doc will initially have the form:
* ```
* {
* docType: 'element',
* name: 'some-name',
* content: 'the content of the file',
* }
* ```
*/
module.exports = function specialElementFileReader() {
return {
name: 'specialElementFileReader',
defaultPattern: /\.md$/,
getDocs: function(fileInfo) {
// We return a single element array because element files only contain one document
return [{
docType: 'element',
name: `<${fileInfo.baseName}>`,
content: fileInfo.content,
}];
}
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Documents attributes that can appear on "special elements", such as `select` on `<ng-content>`.
*
* For example:
*
* ```
* @elementAttribute select="selector"
*
* Only select elements from the projected content that match the given CSS `selector`.
* ```
*/
module.exports = function() {
return {
name: 'elementAttribute',
docProperty: 'attributes',
multi: true,
transforms(doc, tag, value) {
const startOfDescription = value.indexOf('\n');
const name = value.substring(0, startOfDescription).trim();
const description = value.substring(startOfDescription).trim();
return {name, description};
}
};
};
2 changes: 1 addition & 1 deletion aio/tools/transforms/templates/api/base.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</div>
{% block header %}
<header class="api-header">
<h1>{$ doc.name $}</h1>
<h1>{$ doc.name | escape $}</h1>
{% if doc.global %}<label class="api-type-label global">global</label>{% endif %}
<label class="api-type-label {$ doc.docType $}">{$ doc.docType $}</label>
{% if doc.deprecated !== undefined %}<label class="api-status-label deprecated">deprecated</label>{% endif %}
Expand Down
8 changes: 8 additions & 0 deletions aio/tools/transforms/templates/api/element.template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% extends 'export-base.template.html' -%}

{% block overview %}
{% include "includes/element-attributes.html" %}
{% endblock %}
{% block details %}
{% include "includes/description.html" %}
{% endblock %}
4 changes: 2 additions & 2 deletions aio/tools/transforms/templates/api/export-base.template.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{% extends 'base.template.html' -%}

{% block body %}
<section class="short-description">
<section class="short-description">{% block shortDescription %}
{$ doc.shortDescription | marked $}
{% if doc.description %}<p><a href="#description">See more...</a></p>{% endif %}
</section>
{% endblock %}</section>
{% include "includes/security-notes.html" %}
{% include "includes/deprecation.html" %}
{% block overview %}{% endblock %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{%- if doc.attributes %}
<section class="element-attributes">
<h2>Attributes</h2>
<table class="is-full-width list-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{%- for attribute in doc.attributes %}
<tr class="element-attribute">
<td><code>{$ attribute.name $}</code></td>
<td>{$ attribute.description | marked $}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% endif %}
3 changes: 2 additions & 1 deletion aio/tools/transforms/templates/api/package.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h3>{$ title $}</h3>
<table class="is-full-width list-table">
{% for item in filteredItems %}
<tr>
<td><code class="code-anchor{% if item.deprecated %} deprecated-api-item{% endif %}"><a href="{$ overridePath or item.path $}"{%- if item.deprecated != undefined %} class="deprecated-api-item"{% endif %}>{$ item.name $}</a></code></td>
<td><code class="code-anchor{% if item.deprecated %} deprecated-api-item{% endif %}"><a href="{$ overridePath or item.path $}"{%- if item.deprecated != undefined %} class="deprecated-api-item"{% endif %}>{$ item.name | escape $}</a></code></td>
<td>
{% if item.deprecated !== undefined %}{$ ('**Deprecated:** ' + item.deprecated) | marked $}{% endif %}
{% if item.shortDescription %}{$ item.shortDescription | marked $}{% endif %}
Expand Down Expand Up @@ -53,6 +53,7 @@ <h2>{% if doc.isPrimaryPackage %}Primary entry{% else %}Entry{% endif %} point e
{$ listItems(doc.functions, 'Functions') $}
{$ listItems(doc.structures, 'Structures') $}
{$ listItems(doc.directives, 'Directives') $}
{$ listItems(doc.elements, 'Elements') $}
{$ listItems(doc.pipes, 'Pipes') $}
{$ listItems(doc.types, 'Types') $}
{%- endblock %}

0 comments on commit 9c9ae27

Please sign in to comment.