Skip to content

Commit

Permalink
Adds defer boolean prop that when false will synchronously update tags.
Browse files Browse the repository at this point in the history
  • Loading branch information
jaysoo committed Jun 22, 2017
1 parent c947ede commit 0f1cbdb
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 8 deletions.
110 changes: 102 additions & 8 deletions src/HelmetUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,19 +304,55 @@ const handleClientStateChange = newState => {
cancelIdleCallback(_helmetIdleCallback);
}

const splitUpdates = {
baseTag: splitSyncAndDeferred(baseTag),
linkTags: splitSyncAndDeferred(linkTags),
metaTags: splitSyncAndDeferred(metaTags),
noscriptTags: splitSyncAndDeferred(noscriptTags),
scriptTags: splitSyncAndDeferred(scriptTags),
styleTags: splitSyncAndDeferred(styleTags)
};

const syncUpdates = {
baseTag: updateTags(TAG_NAMES.BASE, splitUpdates.baseTag.sync),
linkTags: updateTags(TAG_NAMES.LINK, splitUpdates.linkTags.sync),
metaTags: updateTags(TAG_NAMES.META, splitUpdates.metaTags.sync),
noscriptTags: updateTags(
TAG_NAMES.NOSCRIPT,
splitUpdates.noscriptTags.sync
),
scriptTags: updateTags(TAG_NAMES.SCRIPT, splitUpdates.scriptTags.sync),
styleTags: updateTags(TAG_NAMES.STYLE, splitUpdates.styleTags.sync)
};

_helmetIdleCallback = requestIdleCallback(() => {
updateAttributes(TAG_NAMES.BODY, bodyAttributes);
updateAttributes(TAG_NAMES.HTML, htmlAttributes);

updateTitle(title, titleAttributes);

const tagUpdates = {
baseTag: updateTags(TAG_NAMES.BASE, baseTag),
linkTags: updateTags(TAG_NAMES.LINK, linkTags),
metaTags: updateTags(TAG_NAMES.META, metaTags),
noscriptTags: updateTags(TAG_NAMES.NOSCRIPT, noscriptTags),
scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags),
styleTags: updateTags(TAG_NAMES.STYLE, styleTags)
baseTag: syncUpdates.baseTag.concat(
updateTags(TAG_NAMES.BASE, splitUpdates.baseTag.deferred)
),
linkTags: syncUpdates.linkTags.concat(
updateTags(TAG_NAMES.LINK, splitUpdates.linkTags.deferred)
),
metaTags: syncUpdates.metaTags.concat(
updateTags(TAG_NAMES.META, splitUpdates.metaTags.deferred)
),
noscriptTags: syncUpdates.noscriptTags.concat(
updateTags(
TAG_NAMES.NOSCRIPT,
splitUpdates.noscriptTags.deferred
)
),
scriptTags: syncUpdates.scriptTags.concat(
updateTags(TAG_NAMES.SCRIPT, splitUpdates.scriptTags.deferred)
),
styleTags: syncUpdates.styleTags.concat(
updateTags(TAG_NAMES.STYLE, splitUpdates.styleTags.deferred)
)
};

const addedTags = {};
Expand Down Expand Up @@ -450,10 +486,68 @@ const updateTags = (type, tags) => {
oldTags.forEach(tag => tag.parentNode.removeChild(tag));
newTags.forEach(tag => headElement.appendChild(tag));

return {
return Update({
oldTags,
newTags
};
});
};

// Helper object that implements concat method that takes care of handling
// intersection between old and new tags. (implements Semigroup)
const Update = ({oldTags, newTags}) => {
return Object.create(Object.prototype, {
oldTags: {
enumerable: true,
value: oldTags
},
newTags: {
enumerable: true,
value: newTags
},
concat: {
value: other => {
const mergedNewTags = newTags.concat(other.newTags);
const mergedOldTags = oldTags.concat(other.oldTags);

// Remove intersection between old and new tags since they cancel
// each other out.
for (let i = 0; i < mergedNewTags.length; i++) {
for (let j = 0; j < mergedOldTags.length; j++) {
if (mergedNewTags[i].isEqualNode(mergedOldTags[j])) {
mergedNewTags.splice(i, 1);
mergedOldTags.splice(j, 1);
}
}
}

return Update({
newTags: mergedNewTags,
oldTags: mergedOldTags
});
}
}
});
};

const splitSyncAndDeferred = tags => {
return (
tags &&
tags.reduce(
(acc, tag) => {
// undefined counts as sync
if (tag.defer === false) {
acc.sync.push(tag);
} else {
acc.deferred.push(tag);
}
return acc;
},
{
deferred: [],
sync: []
}
)
);
};

const generateElementAttributesAsString = attributes =>
Expand Down
35 changes: 35 additions & 0 deletions test/HelmetDeclarativeTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2501,6 +2501,41 @@ describe("Helmet - Declarative API", () => {
});
});

describe("deferred tags", () => {
beforeEach(() => {
window.__spy__ = sinon.spy();
});

afterEach(() => {
delete window.__spy__;
});

it("executes synchronously when defer={true} and async otherwise", done => {
ReactDOM.render(
<Helmet>
<script defer={false}>
window.__spy__(1)
</script>
<script defer>
window.__spy__(2)
</script>
<script>
window.__spy__(3)
</script>
</Helmet>,
container
);

expect(window.__spy__.callCount).to.equal(1);

requestIdleCallback(() => {
expect(window.__spy__.callCount).to.equal(3);
expect(window.__spy__.args).to.deep.equal([[1], [2], [3]]);
done();
});
});
});

describe("server", () => {
const stringifiedHtmlAttributes = `lang="ga" class="myClassName"`;
const stringifiedBodyAttributes = `lang="ga" class="myClassName"`;
Expand Down
39 changes: 39 additions & 0 deletions test/HelmetTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2271,6 +2271,45 @@ describe("Helmet", () => {
});
});

describe("deferred tags", () => {
beforeEach(() => {
window.__spy__ = sinon.spy();
});

afterEach(() => {
delete window.__spy__;
});

it("executes synchronously when defer={true} and async otherwise", done => {
ReactDOM.render(
<Helmet
script={[
{
defer: false,
innerHTML: `window.__spy__(1)`
},
{
defer: true,
innerHTML: `window.__spy__(2)`
},
{
innerHTML: `window.__spy__(3)`
}
]}
/>,
container
);

expect(window.__spy__.callCount).to.equal(1);

requestIdleCallback(() => {
expect(window.__spy__.callCount).to.equal(3);
expect(window.__spy__.args).to.deep.equal([[1], [2], [3]]);
done();
});
});
});

describe("server", () => {
const stringifiedHtmlAttributes = `lang="ga" class="myClassName"`;
const stringifiedTitle = `<title ${HELMET_ATTRIBUTE}="true">Dangerous &lt;script&gt; include</title>`;
Expand Down

0 comments on commit 0f1cbdb

Please sign in to comment.