Skip to content

feat(platform browser): introduce Meta service #12322

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

Merged
merged 1 commit into from
Dec 9, 2016

Conversation

DzmitryShylovich
Copy link
Contributor

Fixes #11115

@DzmitryShylovich
Copy link
Contributor Author

CI fails with

+/tmp/angular-build/typings-test.1476534211/node_modules/.bin/tsc -p tsconfig.json
node_modules/@angular/platform-browser/src/browser/meta.d.ts(13,46): error TS1110: Type expected.
node_modules/@angular/platform-browser/src/browser/meta.d.ts(26,44): error TS1110: Type expected.

I don't know what I'm missing.

@vicb vicb added action: review The PR is still awaiting reviews from at least one requested reviewer area: core Issues related to the framework runtime labels Oct 15, 2016
Copy link
Contributor

@jeffbcross jeffbcross left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General question, is there any benefit to setting meta tags in the browser (instead of just pre-render)? Like do Chrome extensions like Pocket look at the current meta tags of content?

* @param contentValue new content value
* @returns {void}
*/
setContent(metaName: string, contentValue: string): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reasoning for not implicitly creating the tag if it doesn't exist? For example, for something like open graph tags, the existence of certain types of tags depends on the page's content. I.e. the og:video tag should not be present if there's no video.

Copy link
Contributor

@FrozenPandaz FrozenPandaz Oct 24, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some meta tags have a "property" and no "name" such as Facebook:

Facebook Open Graph

I know this is not web standard but is this something that should be implemented in like a PropertyMetaTag or something?

W3 Meta Tag

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Almost any respectable application will use property tags as well, even if they are not standard.
If this doesn't get implemented natively, then we'll have to do our own service anyway, which is just duplicating code/effort.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FrozenPandaz I think you're agreeing with my original comment? My comment was positing that the existence of some tags, such as open graph tags, depends on content, so the meta service should manage creating/removing tags instead of just updating attributes of existing tags.

One of my other review comments addresses changing the API to support arbitrary attribute names.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just commenting on the same code..

What I meant is that Metas should not only have a name property but also a... property property.

I do agree with your comment though. There is no one set of metadata for every single app. I believe we should start with a blank slate and have the developer set them 1 by 1.

It should create it if its not there already, edit it if it is there, and it should have the ability to remove it.. I wonder how that will work with SPAs.. we might want to have a removeAll method as well.

* @param metaName meta tag name
* @returns {HTMLElement|null}
*/
getMeta(metaName: string): HTMLElement|null { return getDOM().query(`meta[name=${metaName}]`); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather the get/set methods be more generic than include the getMeta method and return the element. setContent could accept an optional third argument attributeName='content'.

Or is there another reason to have the getMeta method other than setting arbitrary attributes?

getContent(metaName: string): string|null {
const meta: HTMLElement = this.getMeta(metaName);
return meta ? meta.getAttribute(CONTENT_ATTR) : null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A remove method seems appropriate since this service is a map-like interface, as long as the setContent method would implicitly create elements.

@jeffbcross jeffbcross self-assigned this Oct 24, 2016
@jeffbcross
Copy link
Contributor

By the way the TS failures are because of returning union types, which is not compatible with TS 1.8.

@lacolaco
Copy link
Contributor

lacolaco commented Oct 25, 2016

@jeffbcross It's needed to change metadata for sharing content after each routing navigation. This is for second+ views.

expect(actual).toEqual(META_CONTENT);
});

it('should return meta element', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeated description

@zoechi
Copy link
Contributor

zoechi commented Oct 30, 2016

Does this work with server-side rendering and Web Worker when it reads from the DOM?

@jeffbcross
Copy link
Contributor

jeffbcross commented Oct 31, 2016

@laco0416 thanks, I'm looking for documentation about common extensions like pocket to verify that they actually read the meta tags inside the browser, rather than re-requesting the content. Can you provide any links to confirm that popular extensions actually look at the browser state?

@jeffbcross
Copy link
Contributor

@DzmitryShylovich do you plan to update this PR with requested changes?

@kodeine
Copy link

kodeine commented Nov 5, 2016

@DzmitryShylovich @jeffbcross
any idea when this will be fixed and merged? :)

@DzmitryShylovich
Copy link
Contributor Author

@jeffbcross updated

@kodeine
Copy link

kodeine commented Nov 8, 2016

@DzmitryShylovich can you please check, some of the tests didnt pass?

* @param tags
* @returns {HTMLMetaElement[]}
*/
addTag(...tags: MetaDefinition[]): HTMLMetaElement[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we make this addTags and pass an Array always ?

(In the currrent impl isArray is useless)

* @returns {HTMLMetaElement}
*/
getTag(selector: string): HTMLMetaElement {
if (selector == null || selector === '') return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!selector)

* @returns {HTMLMetaElement}
*/
updateTag(selector: string, tag: MetaDefinition): HTMLMetaElement {
let meta: HTMLMetaElement = this.getTag(selector);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

*
* @param selectorOrElem selector or meta element
*/
removeTag(selectorOrElem: string|HTMLMetaElement): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeTagBySelector & removeTagElement

@vicb vicb added action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews pr_state: LGTM and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Nov 10, 2016
@DzmitryShylovich
Copy link
Contributor Author

updated

* @param tags
* @returns {HTMLMetaElement[]}
*/
addTags(...tags: Array<MetaDefinition|MetaDefinition[]>): HTMLMetaElement[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need [][] here ? (over addTags(tags: MD[]))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a convenience so we can add meta tags like

addTags(tag1, tag2, tag3)

or

addTags([tag1, tag2, tag3])

Otherwise we will get such request #7432 :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it addTag and nobody can argue !

*/
removeTagElement(meta: HTMLMetaElement): void {
if (meta) {
this._removeMetaElement(meta);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about inlining methods when called once ony ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree.

*/
getTag(selector: string): HTMLMetaElement {
if (!selector) return null;
return this._dom.query(`meta[${selector}]`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if multiple matching els ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do u think we should use DomAdapter.querySelectorAll here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would tend to say probably (then updateTag makes little sense)

Copy link
Contributor Author

@DzmitryShylovich DzmitryShylovich Nov 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what should we do? I think update is the most wanted method here.

often useful to change meta values as well.

from original issue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example in vuejs they use id to uniquely identify a tag.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateAll ?

Copy link
Contributor Author

@DzmitryShylovich DzmitryShylovich Nov 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this really a problem? http://stackoverflow.com/a/26623223
Looks like it's not possible to have multiple meta tags with the same name/property.

btw not sure how it will work if we dynamically will add tags with the same name...
Just tested in chrome. we can add the same tag over and over again and then query them all as an array :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the spec says ?
Anyway there should be tests (executed BrowserStack and SauceLabs)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec doesn't say anything about multiple tags with the same name https://www.w3.org/TR/html5/document-metadata.html#the-meta-element

*
* ### Example
*
* ```ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very much a bug fan of inline docs but also I think too much docs is bad, can you reduce the # of exs here ?

@DzmitryShylovich
Copy link
Contributor Author

I'll add new commits so it will be easier to review. Will squash them later.

result.push(list[i]);
}
}
return result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return list ? [].slice.call(list) : [];

getDOM().getElementsByTagName(doc, 'head')[0].appendChild(defaultMeta);
});

afterEach(() => getDOM().getElementsByTagName(doc, 'head')[0].removeChild(defaultMeta));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clean up between tests so they won't affect each other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently I use native document to create elements so I need to clean up them between test. Probably I should use fakeDoc = getDOM().createHtmlDocument(); instead

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I meant remove over rempveChild - cleaning up is great !

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it :)

return el;
}

private tryParseSelector(tag: MetaDefinition): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why try here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we are trying our best to create a selector from a tag.
For example, a tag can be {content: 'my content'} (without name or property). In this case we cannot construct selector and I think we should throw an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try (the word) makes me think try (the keyword)

parseSelector() returning null would be ok
parseSelectorOrThrow() throwing would also be ok

*/
@Injectable()
export class Meta {
private _dom: DomAdapter = getDOM();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inject the DOM ? (factory)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to inject dom adapter?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can inject whatever when using a factory fn to define the binding. Makes sense ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do u want me to create a factory provider {provide: DomAdapter: useFactory: getDOM}? :)

@DzmitryShylovich
Copy link
Contributor Author

ie doesn't support remove https://travis-ci.org/angular/angular/jobs/181829865 :)

@vicb
Copy link
Contributor

vicb commented Dec 7, 2016

BrowserDomAdapter.remove() uses removeChild

getDOM().getElementsByTagName(doc, 'head')[0].appendChild(defaultMeta);
});

afterEach(() => defaultMeta.remove());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getDom().remove(defaultMeta);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... or use your own service :)

@DzmitryShylovich
Copy link
Contributor Author

@vicb so what about dom adapter injection? I don't fully understand your point.

@vicb
Copy link
Contributor

vicb commented Dec 8, 2016

// ...
{provide: Meta, useFactory: getMeta}
// ...

function getMeta(): Meta {
  return new Meta(getDom());
}

@DzmitryShylovich
Copy link
Contributor Author

DzmitryShylovich commented Dec 8, 2016

updated (seems circleci is dead)

@vicb vicb added action: merge The PR is ready for merge by the caretaker and removed action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews labels Dec 9, 2016
@vicb
Copy link
Contributor

vicb commented Dec 9, 2016

Thanks !

@vicb vicb merged commit 72361fb into angular:master Dec 9, 2016
@MarkPieszak
Copy link

Do we know if this is slated for 2.4? @vicb

@jeffbcross
Copy link
Contributor

@MarkPieszak I believe there is no 2.4 release planned (see 2.3 blog post) so this would be in the first beta for 4.0 (See @IgorMinar's talk on the subject).

@MarkPieszak
Copy link

Family over past few days, I saw something about that, but thought it was an April Fools joke (in December) haha.

So March then, great! I was just wondering since this makes life easier for Universal users, think we talked about that the other week.

@jeffbcross
Copy link
Contributor

@MarkPieszak yep, a big motivator of this API was for pre-rendering. Final will be in March, but betas will start being released soon :).

@MarkPieszak
Copy link

@jeffbcross great! Messaged you on the slack, not sure if you still use that one

@kodeine
Copy link

kodeine commented Jan 25, 2017

@DzmitryShylovich is there any docs on how to use this? Also which version of angular this is supported?

@DzmitryShylovich
Copy link
Contributor Author

@kodeine

is there any docs on how to use this?

not yet. you can find usage examples in the tests

which version of angular this is supported?

beta 4.0

@goelinsights
Copy link

goelinsights commented Apr 7, 2017

@DzmitryShylovich @jeffbcross was looking at the new API for meta -- is it possible to use it for link rel= tags as well?

Or should we stick with the old pattern along the lines below. If the second, do you know what where the old getDOM moved to?...this import broke for me

import { getDOM } from '@angular/platform-browser/src/dom/dom_adapter';

// ...
this.DOM = getDOM();

private getOrCreateLinkElement(name: string): HTMLElement {
  let el: HTMLElement;
  el = this.DOM.query('link[rel=' + name + ']');
  if (el === null) {
    el = this.DOM.createElement('link');
    el.setAttribute('rel', name);
    this.headElement.appendChild(el);
  }
  return el;
}

@MarkPieszak
Copy link

Check out here for a temporary solution until a DocumentService is implemented within Core
#15776 (comment)

@fulls1z3
Copy link

@MarkPieszak I checked the linkService and it seems awesome.

Actualy I am planning to use such a similar htmlService to set the lang attribute of html tag (to the working language) as well as canonical links, right at the meta utility (@nglibs/meta) generating the meta tags based on route definitions.

Meanwhile, many thanks to @DzmitryShylovich for Meta/Title services. This way, the meta utility avoids direct manipulation of DOM and just depends on DomAdapter (in Angular 2) or on Meta/Title services (in Angular 4) - and works well with server-side rendering as well as client-side rendering.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 11, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime cla: yes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implementing a Meta class for getting and changing HTML meta tags