forked from vercel/next.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
268 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,69 @@ | ||
import HTMLDOMPropertyConfig from 'react/lib/HTMLDOMPropertyConfig' | ||
|
||
const DEFAULT_TITLE = '' | ||
|
||
export default class HeadManager { | ||
updateHead (head) { | ||
const tags = {} | ||
head.forEach((h) => { | ||
const components = tags[h.type] || [] | ||
components.push(h) | ||
tags[h.type] = components | ||
}) | ||
|
||
this.updateTitle(tags.title ? tags.title[0] : null) | ||
|
||
const types = ['meta', 'base', 'link', 'style', 'script'] | ||
types.forEach((type) => { | ||
this.updateElements(type, tags[type] || []) | ||
}) | ||
} | ||
|
||
updateTitle (component) { | ||
let title | ||
if (component) { | ||
const { children } = component.props | ||
title = 'string' === typeof children ? children : children.join('') | ||
} else { | ||
title = DEFAULT_TITLE | ||
} | ||
if (title !== document.title) document.title = title | ||
} | ||
|
||
updateElements (type, components) { | ||
const headEl = document.getElementsByTagName('head')[0] | ||
const oldTags = Array.prototype.slice.call(headEl.querySelectorAll(type '.next-head')) | ||
const newTags = components.map(reactElementToDOM).filter((newTag) => { | ||
for (let i = 0, len = oldTags.length; i < len; i ) { | ||
const oldTag = oldTags[i] | ||
if (oldTag.isEqualNode(newTag)) { | ||
oldTags.splice(i, 1) | ||
return false | ||
} | ||
} | ||
return true | ||
}) | ||
|
||
oldTags.forEach((t) => t.parentNode.removeChild(t)) | ||
newTags.forEach((t) => headEl.appendChild(t)) | ||
} | ||
} | ||
|
||
function reactElementToDOM ({ type, props }) { | ||
const el = document.createElement(type) | ||
for (const p in props) { | ||
if (!props.hasOwnProperty(p)) continue | ||
if ('children' === p || 'dangerouslySetInnerHTML' === p) continue | ||
|
||
const attr = HTMLDOMPropertyConfig.DOMAttributeNames[p] || p.toLowerCase() | ||
el.setAttribute(attr, props[p]) | ||
} | ||
|
||
const { children, dangerouslySetInnerHTML } = props | ||
if (dangerouslySetInnerHTML) { | ||
el.innerHTML = dangerouslySetInnerHTML.__html || '' | ||
} else if (children) { | ||
el.textContent = 'string' === typeof children ? children : children.join('') | ||
} | ||
return el | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,82 @@ | ||
import React from 'react' | ||
import sideEffect from './side-effect' | ||
|
||
class Head extends React.Component { | ||
static contextTypes = { | ||
headManager: React.PropTypes.object | ||
} | ||
|
||
render () { | ||
return null | ||
} | ||
|
||
componentWillUnmount () { | ||
this.context.headManager.updateHead([]) | ||
} | ||
} | ||
|
||
function reduceComponents (components) { | ||
return components | ||
.map((c) => c.props.children) | ||
.filter((c) => !!c) | ||
.map((children) => React.Children.toArray(children)) | ||
.reduce((a, b) => a.concat(b), []) | ||
.reverse() | ||
.filter(unique()) | ||
.reverse() | ||
.map((c) => { | ||
const className = (c.className ? c.className ' ' : '') 'next-head' | ||
return React.cloneElement(c, { className }) | ||
}) | ||
} | ||
|
||
function mapOnServer (head) { | ||
return head | ||
} | ||
|
||
function onStateChange (head) { | ||
if (this.context && this.context.headManager) { | ||
this.context.headManager.updateHead(head) | ||
} | ||
} | ||
|
||
const METATYPES = ['name', 'httpEquiv', 'charSet', 'itemProp'] | ||
|
||
// returns a function for filtering head child elements | ||
// which shouldn't be duplicated, like <title/>. | ||
|
||
function unique () { | ||
const tags = new Set() | ||
const metaTypes = new Set() | ||
const metaCategories = {} | ||
|
||
return (h) => { | ||
switch (h.type) { | ||
case 'title': | ||
case 'base': | ||
if (tags.has(h.type)) return false | ||
tags.add(h.type) | ||
break | ||
case 'meta': | ||
for (let i = 0, len = METATYPES.length; i < len; i ) { | ||
const metatype = METATYPES[i] | ||
if (!h.props.hasOwnProperty(metatype)) continue | ||
|
||
if ('charSet' === metatype) { | ||
if (metaTypes.has(metatype)) return false | ||
metaTypes.add(metatype) | ||
} else { | ||
const category = h.props[metatype] | ||
const categories = metaCategories[metatype] || new Set() | ||
if (categories.has(category)) return false | ||
categories.add(category) | ||
metaCategories[metatype] = categories | ||
} | ||
} | ||
break | ||
} | ||
return true | ||
} | ||
} | ||
|
||
export default sideEffect(reduceComponents, onStateChange, mapOnServer)(Head) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,101 @@ | ||
import React, { Component } from 'react' | ||
|
||
export default function withSideEffect (reduceComponentsToState, handleStateChangeOnClient, mapStateOnServer) { | ||
if (typeof reduceComponentsToState !== 'function') { | ||
throw new Error('Expected reduceComponentsToState to be a function.') | ||
} | ||
|
||
if (typeof handleStateChangeOnClient !== 'function') { | ||
throw new Error('Expected handleStateChangeOnClient to be a function.') | ||
} | ||
|
||
if (typeof mapStateOnServer !== 'undefined' && typeof mapStateOnServer !== 'function') { | ||
throw new Error('Expected mapStateOnServer to either be undefined or a function.') | ||
} | ||
|
||
function getDisplayName (WrappedComponent) { | ||
return WrappedComponent.displayName || WrappedComponent.name || 'Component' | ||
} | ||
|
||
return function wrap (WrappedComponent) { | ||
if (typeof WrappedComponent !== 'function') { | ||
throw new Error('Expected WrappedComponent to be a React component.') | ||
} | ||
|
||
const mountedInstances = new Set() | ||
let state | ||
let shouldEmitChange = false | ||
|
||
function emitChange (component) { | ||
state = reduceComponentsToState([...mountedInstances]) | ||
|
||
if (SideEffect.canUseDOM) { | ||
handleStateChangeOnClient.call(component, state) | ||
} else if (mapStateOnServer) { | ||
state = mapStateOnServer(state) | ||
} | ||
} | ||
|
||
function maybeEmitChange (component) { | ||
if (!shouldEmitChange) return | ||
shouldEmitChange = false | ||
emitChange(component) | ||
} | ||
|
||
class SideEffect extends Component { | ||
// Try to use displayName of wrapped component | ||
static displayName = `SideEffect(${getDisplayName(WrappedComponent)})` | ||
|
||
static contextTypes = WrappedComponent.contextTypes | ||
|
||
// Expose canUseDOM so tests can monkeypatch it | ||
static canUseDOM = 'undefined' !== typeof window | ||
|
||
static peek () { | ||
return state | ||
} | ||
|
||
static rewind () { | ||
if (SideEffect.canUseDOM) { | ||
throw new Error('You may only call rewind() on the server. Call peek() to read the current state.') | ||
} | ||
|
||
maybeEmitChange() | ||
|
||
const recordedState = state | ||
state = undefined | ||
mountedInstances.clear() | ||
return recordedState | ||
} | ||
|
||
componentWillMount () { | ||
mountedInstances.add(this) | ||
shouldEmitChange = true | ||
} | ||
|
||
componentDidMount () { | ||
maybeEmitChange(this) | ||
} | ||
|
||
componentWillUpdate () { | ||
shouldEmitChange = true | ||
} | ||
|
||
componentDidUpdate () { | ||
maybeEmitChange(this) | ||
} | ||
|
||
componentWillUnmount () { | ||
mountedInstances.delete(this) | ||
shouldEmitChange = false | ||
emitChange(this) | ||
} | ||
|
||
render () { | ||
return <WrappedComponent>{ this.props.children }</WrappedComponent> | ||
} | ||
} | ||
|
||
return SideEffect | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters