-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Add tweet block for embedding tweets #754
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
Changes from 1 commit
3cd38d1
abc74cd
5fa3cb8
8c38062
09814e5
eedb7d7
ce8fc70
5fd1bb9
0aad93d
5ce13f6
a43001f
c223f18
ff12c83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,17 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import jQuery from 'jquery'; | ||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import Sandbox from 'components/sandbox'; | ||
| import Button from 'components/button'; | ||
| import Placeholder from 'components/placeholder'; | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { registerBlock, query } from '../../api'; | ||
| import Sandbox from '../../../components/sandbox'; | ||
| import Button from '../../../components/button'; | ||
| import Placeholder from '../../../components/placeholder'; | ||
|
|
||
| const { prop } = query; | ||
|
|
||
|
|
@@ -14,55 +21,72 @@ registerBlock( 'core/tweet', { | |
|
|
||
| category: 'social', | ||
|
|
||
| attributes: { | ||
| url: prop( '*', 'innerHTML' ), // our html is just a div with the url in, for WP's oembed to process | ||
| attributes( url ) { | ||
| return { url }; | ||
| }, | ||
|
|
||
| edit: class extends wp.element.Component { | ||
| constructor() { | ||
| super( ...arguments ); | ||
| this.fetchTweet = this.fetchTweet.bind( this ); | ||
| // Copies the block's url so we can edit it without having the block | ||
| // update (i.e. refetch the tweet) every time it changes in this edit component. | ||
| this.state = { | ||
| url: this.props.attributes.url, | ||
| html: '', | ||
| error: false, | ||
| fetching: false, | ||
| }; | ||
| } | ||
| doFetch( url, setAttributes, setState ) { | ||
| setState( { fetching: true, error: false } ); | ||
| jQuery.ajax( { | ||
| doFetch( url ) { | ||
| this.setState( { fetching: true, error: false } ); | ||
| this.fetchXHR = jQuery.ajax( { | ||
| type: 'GET', | ||
| dataType: 'jsonp', | ||
| data: {}, | ||
| timeout: 5000, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the default
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately we do need the timeout. JSONP requests are horrible. If they fail, they don't call the error callback, so the only way to do it is to specify a large-ish timeout and wait for it to fail.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But is the issue of wanting to avoid waiting a long time for requests specific to this block? I'm curious if and what the default timeout is, or if this should be applied globally through More a consistency question than anything.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to impose a timeout, or the error handling simply does not work. It shouldn't be applied globally though ajaxSetup because this is specific to JSONP requests. From http://api.jquery.com/jQuery.ajax/
There doesn't seem to be a default timeout value. The other way of dealing with errors for cross domain requests is a jQuery plugin, https://github.com/jaubourg/jquery-jsonp , but it doesn't seem to be maintained. |
||
| url: 'https://publish.twitter.com/oembed?url=' + encodeURI( url ), | ||
| error: function() { | ||
| setState( { fetching: false, error: true } ); | ||
| }, | ||
| success: function( msg ) { | ||
| setAttributes( { url: url } ); | ||
| setState( { fetching: false, error: false, html: msg.html } ); | ||
| error: () => this.setState( { fetching: false, error: true } ), | ||
| success: ( msg ) => { | ||
| this.props.setAttributes( { url } ); | ||
| this.setState( { fetching: false, error: false, html: msg.html } ); | ||
| }, | ||
| } ); | ||
| } | ||
| componentDidMount() { | ||
| if ( this.state.url ) { | ||
| this.doFetch( this.state.url, this.props.setAttributes, this.setState.bind( this ) ); | ||
| this.doFetch( this.state.url ); | ||
| } | ||
| } | ||
| componentWillUnmount() { | ||
| if ( this.fetchXHR ) { | ||
| this.fetchXHR.abort(); | ||
| delete this.fetchXHR; | ||
| } | ||
| } | ||
| fetchTweet() { | ||
| fetchTweet( event ) { | ||
| const { url } = this.state; | ||
| this.doFetch( url, this.props.setAttributes, this.setState.bind( this ) ); | ||
| event.preventDefault(); | ||
| this.doFetch( url ); | ||
| } | ||
| render() { | ||
| const { html, url, error, fetching } = this.state; | ||
|
|
||
| if ( ! html ) { | ||
| if ( html ) { | ||
| const author = this.state.url.split( '/' )[3]; | ||
| /* translators: {AUTHOR}: username of the tweet's author */ | ||
| const __title = wp.i18n.__( 'Tweet from {AUTHOR}' ); | ||
| const title = __title.replace( '{AUTHOR}', author ); | ||
| return ( | ||
| <Placeholder icon="twitter" label={ wp.i18n.__( 'Twitter' ) } className="blocks-tweet"> | ||
| <Sandbox | ||
| html={ html } | ||
| title={ title } /> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <Placeholder icon="twitter" label={ wp.i18n.__( 'Twitter' ) } className="blocks-tweet"> | ||
| <form onSubmit={ this.fetchTweet }> | ||
| <input | ||
| type="text" | ||
| className="components-placeholder__input" | ||
| value={ url } | ||
| placeholder={ wp.i18n.__( 'Enter tweet URL to embed...' ) } | ||
|
|
@@ -71,27 +95,22 @@ registerBlock( 'core/tweet', { | |
| ( | ||
| <Button | ||
| isLarge | ||
| onClick={ this.fetchTweet }> | ||
| type="submit"> | ||
| { wp.i18n.__( 'Embed' ) } | ||
| </Button> | ||
| ) : ( | ||
| <span className="spinner is-active" /> | ||
| ) | ||
| } | ||
| { error && ( <p className="components-placeholder__error">{ wp.i18n.__( 'Sorry, we couldn\'t fetch that tweet.' ) }</p> ) } | ||
| </Placeholder> | ||
| ); | ||
| } | ||
| return ( | ||
| <Sandbox html={ html } /> | ||
| </form> | ||
| { error && ( <p className="components-placeholder__error">{ wp.i18n.__( 'Sorry, we couldn\'t fetch that tweet.' ) }</p> ) } | ||
| </Placeholder> | ||
| ); | ||
| } | ||
| }, | ||
|
|
||
| save( { attributes } ) { | ||
| const { url } = attributes; | ||
| return ( | ||
| <div>{ url }</div> | ||
| ); | ||
| return 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.
It's discouraged to create copies of props in state because it muddies the fact that the source of truth is the prop itself, leaving you responsible for maintaining when that prop changes. And evidenced by a lack of
componentWillReceivePropsorcomponentDidUpdate, it appears we're not.Instead, where you're currently using
this.state.url, you should try to referencethis.props.attributes.urldirectly instead.And depending on if we want the component to trigger another request if the URL ever changes, it'd be good to bind to
componentDidUpdateto check if the URL has changed and firedoFetchagain.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.
So when the button is clicked (or form submitted) I'd use a reference to the text input to set the url, instead of having it set when the text input changes?
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.
Ah, I think I overlooked that. With that in mind, it can be fine to create a copy.
Here's an archived (some outdated syntax) article which explains more of the context and highlights this exception:
https://github.com/facebook/react/blob/8cac523/docs/tips/10-props-in-getInitialState-as-anti-pattern.md
Since we don't have as much control over the naming of the incoming prop, except if we were to create an intermediary component or rename the attribute (both of which don't seem ideal), I think it'd be fine to ignore the
initialUrlrecommendation here.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'll add a comment in the constructor to make it clear what we're doing here.