feat(docs-infra): add support for "special elements" (#41299)

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
This commit is contained in:
Pete Bacon Darwin 2021-03-21 11:07:34 +00:00 committed by Alex Rickabaugh
parent 2d1347b2ce
commit 62aca30286
19 changed files with 202 additions and 26 deletions

View File

@ -412,7 +412,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:

View File

@ -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.
```

View File

@ -0,0 +1 @@
The `<ng-container>` can be used to hold directives without creating an HTML element.

View File

@ -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`.

View File

@ -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.

View File

@ -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"},

View File

@ -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*",

View File

@ -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[] = [

View File

@ -38,6 +38,7 @@ $darkorange: #940;
$anti-pattern: $brightred;
// API & CODE COLORS
$amber-200: #AA3000;
$amber-700: #FFA000;
$blue-400: #42A5F5;
$blue-500: #2196F3;
@ -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

View File

@ -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])
@ -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'))
@ -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']);
})
/**
@ -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 = '.';
@ -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'
},
{
basePath: API_SOURCE_PATH,
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 + '/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
@ -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;
})
@ -186,8 +212,6 @@ module.exports =
.config(function(computePathsProcessor, EXPORT_DOC_TYPES, generateApiListDoc) {
const API_SEGMENT = 'api';
generateApiListDoc.outputFolder = API_SEGMENT;
computePathsProcessor.pathTemplates.push({

View File

@ -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.';
}

View File

@ -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);
}
});
}
};
};

View File

@ -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,
}];
}
};
};

View File

@ -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};
}
};
};

View File

@ -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 %}

View File

@ -0,0 +1,8 @@
{% extends 'export-base.template.html' -%}
{% block overview %}
{% include "includes/element-attributes.html" %}
{% endblock %}
{% block details %}
{% include "includes/description.html" %}
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -8,7 +8,7 @@
<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 %}
@ -53,6 +53,7 @@
{$ 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 %}