-
Notifications
You must be signed in to change notification settings - Fork 26.6k
feat(aio): add embedded TOC component #16078
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
Conversation
01af6f0
to
415f3fc
Compare
The angular.io preview for 415f3fc76ea21580942c1ee0f543d4e02a5a531c is available here. |
It can use some styling love, but generally LGTM. I think it is quite useful (especially for longer pages). |
@@ -19,7 +19,7 @@ import { Component, ElementRef, OnInit } from '@angular/core'; | |||
<aio-code [ngClass]="{'headed-code':title, 'simple-code':!title}" [code]="code" [language]="language" [linenums]="linenums"></aio-code> | |||
` | |||
}) | |||
export class CodeExampleComponent implements OnInit { // implements AfterViewInit { | |||
export class CodeExampleComponent implements OnInit { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you specifically want to create these TOCs manually? Would it not be cleaner (and less maintenance for authors) if we parsed the document after it is rendered and genearate the TOC from all the H2s, etc.
I agree with Pete, TOC should be auto generated by dgeni. We should just put a placeholder for dgeni to know where to drop the TOC and then dgeni can do the rest (especially once the header anchor PR is merged, this will be trivial and less error prone) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Pete, TOC should be auto generated by dgeni.
We should just put a placeholder for dgeni to know where to drop the TOC and then dgeni can do the rest (especially once the header anchor PR is merged, this will be trivial and less error prone)
415f3fc
to
0581920
Compare
The angular.io preview for 05819207b05e66e181a7b81e80b944443c8fd3df is available here. |
Regarding TOC generation. I thought about that when I started this journey. I should have explained in the opening comment. I have updated it with our ( @petebacondarwin and my) plan. |
4ec6987
to
88a996e
Compare
Ready to explore. No tests yet so still WIP.
The TOC generator will use heading ids if present. If missing, it creates them. |
build failed, so there is no preview. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please explain why we should implement this functionality in dgeni? it already does all the anchor handling and link checking, so it's only natural that it will also do TOC generation.
aio/src/app/shared/toc.service.ts
Outdated
|
||
setDoc(doc: DocumentContents, docElement: ElementRef) { | ||
this.doc = doc; | ||
this.docElement = docElement.nativeElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe rename the docElement
to something else? it's confusing that you have two variables of the same name but different types.
aio/src/app/shared/toc.service.ts
Outdated
setDoc(doc: DocumentContents, docElement: ElementRef) { | ||
this.doc = doc; | ||
this.docElement = docElement.nativeElement; | ||
this.url = doc.url || ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
url -> docUrl?
hasContent = true; // assume it does until we know otherwise | ||
hasSecondary = true; | ||
isClosed = true; | ||
isEmbedded = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the difference between embedded
and isEmbedded
?
aio/src/app/shared/toc.service.ts
Outdated
// is always authored by the documentation team and is considered to be safe | ||
const li = this.document.createElement('li'); | ||
const html = `<a href="${this.url}#${id}" title="${heading.innerText}">${heading.innerHTML}</a>`; | ||
li.innerHTML = html; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do you use innerHTML here? It's unnecessary dangerous and error prone. In the other places we didn't have another option, so we had to use innerHTML, but here we can use regular Angular templating.
aio/src/app/shared/toc.service.ts
Outdated
} | ||
|
||
// security: the document which provides this heading content | ||
// is always authored by the documentation team and is considered to be safe |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not true. it can't be considered safe. you are mixing various inputs into a string that will result in html and later dom. it's hard to confidently say that all combinations of possible data will be safe when concatenated below. It will take just one heading to contain double quote and your string concat will break.
aio/src/app/shared/toc.service.ts
Outdated
return tocElements; | ||
} | ||
|
||
private getId(h: HTMLHeadingElement, idMap: Map<string, number>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove per comment above
} | ||
|
||
private setTocList(el: Element) { | ||
setTimeout(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a big red flag. Whatever you are doing here is likely either bad design or a bug in Angular. Since I see that you are recreating your own templating engine above, I suggest that you stop doing that and use Angular templating instead of custom dom manipulation.
// Worse to use `AfterViewInit` because several bound properties can change | ||
// triggering the dreaded "property value changed after checked" error. | ||
|
||
// Security: the aioTocContent comes from the pre-rendered DOM and is considered to be secure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not true. by now you have no idea what is being appended because you processed the DOM in many ways and you don't know the origin and interaction of various interpolated parts.
@@ -0,0 +1,23 @@ | |||
// TODO: WRITE TESTS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -0,0 +1,20 @@ | |||
<div *ngIf="hasContent" [class.embedded]="isEmbedded" [class.closed]="isClosed"> | |||
<div *ngIf="!hasSecondary"class="toc-heading">Contents</div> | |||
<div *ngIf="hasSecondary" class="toc-heading secondary" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this really need to be this complicated? it's just a TOC :-)
why do we need the ability to close the TOC? if we really do need that functionality, shouldn't we use the material zippy instead?
const content = this.elementRef.nativeElement.aioTocContent.trim(); | ||
this.hasContent = !!content; | ||
|
||
if (this.hasContent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we really want to allow manually created TOC? why have two ways of doing this? Let's just have one. Can we remove this option?
I spoke with @IgorMinar. |
88a996e
to
880dc39
Compare
33b9108
to
5a828cc
Compare
The angular.io preview for bdc0ccac4dff1129ba3e1bcad15e26a33c2f9fc1 is available here. |
@@ -0,0 +1,215 @@ | |||
// TODO: WRITE TESTS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps you can remove this now?
let title = ''; | ||
const titleEl = this.hostElement.querySelector('h1'); | ||
// Only create TOC for docs with an <h1> title | ||
// If you don't want a TOC, don't have an <h1> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The home page will have an h1
right? But I guess it shouldn't have a TOC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be marked with no-toc
class and thus skipped
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is worth putting into the comment for posterity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At a very high level this looks awesome! I'll pick through it tonight.
bdc0cca
to
40e6e2c
Compare
Addressed all points. We all agreed to do the TOC in the client.
40e6e2c
to
395476d
Compare
The angular.io preview for 40e6e2c281194dcb6bf7bd4c9899b00860b689ed is available here. |
The angular.io preview for 395476d27975b274293719c4addbd9c358e9f81d is available here. |
395476d
to
eb28382
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR works for me. I couldn't see any major issues with the implementation. I guess we need to implement the floating ToC now too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file-not-found
and fetching-error
content need a no-toc
classes.
@@ -0,0 +1,24 @@ | |||
<div *ngIf="hasToc" [class.closed]="isClosed"> | |||
<div *ngIf="!hasSecondary"class="toc-heading">Contents</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Missing space before class
(unless it is intentional to align with the one below 😛)
let title = ''; | ||
const titleEl = this.hostElement.querySelector('h1'); | ||
// Only create TOC for docs with an <h1> title | ||
// If you don't want a TOC, don't have an <h1> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is worth putting into the comment for posterity.
} | ||
|
||
@media screen and (max-width: 1200px) { | ||
aio-toc.embedded:not(:empty) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This selector does not work. :empty
will not match even if there is only whitespace in the element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied it from the google web doc example and don't know what it does. I'm not testing it either. Removing.
… documents Related to angular#16078.
… documents Related to #16078.
… documents Related to angular#16078.
… documents Related to angular#16078.
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
WHAT IS IT?
Adds an expando embedded Table of Contents component (
<aio-toc>
) that works much likethis google web documentation page.
Update April 26
The latest commit is a complete rewrite based on feedback and discussion. The older commits have been squashed.
The current PR has the following characteristics:
<h1>
which serves as the document title.no-toc
class to the<h1>
. That's important for many pages, especially marketing pages.TocComponent
uses standard templating to display the TOC info<h1>
This PR does not yet display the TOC in a third panel on the right when the viewport is wide.
Original Commits
Second commit shows the
<aio-toc>
component at work in the "Angular Modules" guide,ngModules.md
.Generating the TOC
Sometimes the author wants to control the TOC by hand which is why this component can and will prefer to use projected content.
THEN ... in a subsequent PR, we are going to tackle how to generate the TOC when the author simply drops
<aio-toc></aio-toc>
on the page where the TOC should go.I discussed separately with @petebacondarwin whether dgeni or the app should generate the TOC-content when the author omits it. While dgeni can do the job ahead of time, it can't account for the potential expansion of runtime, embedded components which might add headings to the page.
Moreover it is possible that the TOC component will move around. It's in the doc body in normal width but on wide screens it could appear in a 3rd panel to the right as it does on the Google Web docs.
Accordingly, we agreed that the doc viewer will calculate the TOC (if needed) with the aid of a service, after embedded components have rendered.
That is Part 2 of this PR (in the 3rd commit). The "Animations" page demonstrates TOC generation.
Remember to run
yarn docs
to see it in action.Todo
Please check if the PR fulfills these requirements
What kind of change does this PR introduce? (check one with "x")