Prevent infinite redraw loop in IE

Welp, this is probably the most subtle bug I've ever tracked down and fixed.

Turns out that IE has this bug where the "oninput" event will be triggered whenever the "placeholder" attribute is changed. Most placeholders get their value from app.trans. The app.trans method returns a VirtualElement – which is an array, not a string! That means when Mithril's diffing algorithm was comparing the old value to the new value, it was comparing two different array instances, and thus deciding the value was dirty and the placeholder attribute needed to be updated. Due to the IE bug, that was leading to the "oninput" event being triggered... and then through Mithril's auto-redraw mechanism, a redraw would be triggered, and so the cycle continued.

Since the inputs in the LogInModal (among others) only update the component state on the "onchange" event (i.e. when the input loses focus), the intermittent redraws would cause the input's value to be cleared continuously. That's what was causing #464. Could've been easily and superficially patched by changing them to use "oninput" events, but luckily I dived a little deeper!

Glad that's over. Running IE11's buggy dev tools in an underpowered VM isn't fun. Would not recommend.

closes #464
This commit is contained in:
Toby Zerner 2015-09-25 23:44:15 +09:30
parent 9347d79913
commit 9b51edc939
8 changed files with 24 additions and 16 deletions

View File

@ -1,4 +1,5 @@
import ComposerBody from 'flarum/components/ComposerBody';
import extractText from 'flarum/utils/extractText';
/**
* The `DiscussionComposer` component displays the composer content for starting
@ -26,10 +27,10 @@ export default class DiscussionComposer extends ComposerBody {
static initProps(props) {
super.initProps(props);
props.placeholder = props.placeholder || app.trans('core.composer_discussion_body_placeholder');
props.placeholder = props.placeholder || extractText(app.trans('core.composer_discussion_body_placeholder'));
props.submitLabel = props.submitLabel || app.trans('core.composer_discussion_submit_button');
props.confirmExit = props.confirmExit || app.trans('core.composer_discussion_discard_confirmation');
props.titlePlaceholder = props.titlePlaceholder || app.trans('core.composer_discussion_title_placeholder');
props.confirmExit = props.confirmExit || extractText(app.trans('core.composer_discussion_discard_confirmation'));
props.titlePlaceholder = props.titlePlaceholder || extractText(app.trans('core.composer_discussion_title_placeholder'));
}
headerItems() {

View File

@ -2,6 +2,7 @@ import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
import GroupBadge from 'flarum/components/GroupBadge';
import Group from 'flarum/models/Group';
import extractText from 'flarum/utils/extractText';
/**
* The `EditUserModal` component displays a modal dialog with a login form.
@ -37,7 +38,7 @@ export default class EditUserModal extends Modal {
<div className="Form">
<div className="Form-group">
<label>Username</label>
<input className="FormControl" placeholder={app.trans('core.edit_user_username_label')}
<input className="FormControl" placeholder={extractText(app.trans('core.edit_user_username_label'))}
value={this.username()}
onchange={m.withAttr('value', this.username)} />
</div>
@ -45,7 +46,7 @@ export default class EditUserModal extends Modal {
<div className="Form-group">
<label>Email</label>
<div>
<input className="FormControl" placeholder={app.trans('core.edit_user_email_label')}
<input className="FormControl" placeholder={extractText(app.trans('core.edit_user_email_label'))}
value={this.email()}
onchange={m.withAttr('value', this.email)} />
</div>
@ -64,7 +65,7 @@ export default class EditUserModal extends Modal {
Set new password
</label>
{this.setPassword() ? (
<input className="FormControl" type="password" name="password" placeholder={app.trans('core.edit_user_password_label')}
<input className="FormControl" type="password" name="password" placeholder={extractText(app.trans('core.edit_user_password_label'))}
value={this.password()}
onchange={m.withAttr('value', this.password)} />
) : ''}

View File

@ -1,6 +1,7 @@
import Modal from 'flarum/components/Modal';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import extractText from 'flarum/utils/extractText';
/**
* The `ForgotPasswordModal` component displays a modal which allows the user to
@ -60,7 +61,7 @@ export default class ForgotPasswordModal extends Modal {
<div className="Form Form--centered">
<p className="helpText">{app.trans('core.forgot_password_text')}</p>
<div className="Form-group">
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.forgot_password_email_placeholder')}
<input className="FormControl" name="email" type="email" placeholder={extractText(app.trans('core.forgot_password_email_placeholder'))}
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />

View File

@ -4,6 +4,7 @@ import SignUpModal from 'flarum/components/SignUpModal';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import LogInButtons from 'flarum/components/LogInButtons';
import extractText from 'flarum/utils/extractText';
/**
* The `LogInModal` component displays a modal dialog with a login form.
@ -47,14 +48,14 @@ export default class LogInModal extends Modal {
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="email" placeholder={app.trans('core.log_in_username_or_email_placeholder')}
<input className="FormControl" name="email" placeholder={extractText(app.trans('core.log_in_username_or_email_placeholder'))}
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
</div>
<div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.log_in_password_placeholder')}
<input className="FormControl" name="password" type="password" placeholder={extractText(app.trans('core.log_in_password_placeholder'))}
value={this.password()}
onchange={m.withAttr('value', this.password)}
disabled={this.loading} />

View File

@ -2,6 +2,7 @@ import ComposerBody from 'flarum/components/ComposerBody';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import icon from 'flarum/helpers/icon';
import extractText from 'flarum/utils/extractText';
/**
* The `ReplyComposer` component displays the composer content for replying to a
@ -24,9 +25,9 @@ export default class ReplyComposer extends ComposerBody {
static initProps(props) {
super.initProps(props);
props.placeholder = props.placeholder || app.trans('core.composer_reply_body_placeholder');
props.placeholder = props.placeholder || extractText(app.trans('core.composer_reply_body_placeholder'));
props.submitLabel = props.submitLabel || app.trans('core.composer_reply_submit_button');
props.confirmExit = props.confirmExit || app.trans('core.composer_reply_discard_confirmation');
props.confirmExit = props.confirmExit || extractText(app.trans('core.composer_reply_discard_confirmation'));
}
headerItems() {

View File

@ -2,6 +2,7 @@ import Component from 'flarum/Component';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import ItemList from 'flarum/utils/ItemList';
import classList from 'flarum/utils/classList';
import extractText from 'flarum/utils/extractText';
import icon from 'flarum/helpers/icon';
import DiscussionsSearchSource from 'flarum/components/DiscussionsSearchSource';
import UsersSearchSource from 'flarum/components/UsersSearchSource';
@ -83,7 +84,7 @@ export default class Search extends Component {
})}>
<div className="Search-input">
<input className="FormControl"
placeholder={app.trans('core.header_search_placeholder')}
placeholder={extractText(app.trans('core.header_search_placeholder'))}
value={this.value()}
oninput={m.withAttr('value', this.value)}
onfocus={() => this.hasFocus = true}

View File

@ -3,6 +3,7 @@ import LogInModal from 'flarum/components/LogInModal';
import avatar from 'flarum/helpers/avatar';
import Button from 'flarum/components/Button';
import LogInButtons from 'flarum/components/LogInButtons';
import extractText from 'flarum/utils/extractText';
/**
* The `SignUpModal` component displays a modal dialog with a singup form.
@ -72,14 +73,14 @@ export default class SignUpModal extends Modal {
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="username" placeholder={app.trans('core.sign_up_username_placeholder')}
<input className="FormControl" name="username" placeholder={extractText(app.trans('core.sign_up_username_placeholder'))}
value={this.username()}
onchange={m.withAttr('value', this.username)}
disabled={this.loading} />
</div>
<div className="Form-group">
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.sign_up_email_placeholder')}
<input className="FormControl" name="email" type="email" placeholder={extractText(app.trans('core.sign_up_email_placeholder'))}
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading || (this.props.token && this.props.email)} />
@ -87,7 +88,7 @@ export default class SignUpModal extends Modal {
{this.props.token ? '' : (
<div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.sign_up_password_placeholder')}
<input className="FormControl" name="password" type="password" placeholder={extractText(app.trans('core.sign_up_password_placeholder'))}
value={this.password()}
onchange={m.withAttr('value', this.password)}
disabled={this.loading} />

View File

@ -1,6 +1,7 @@
import Component from 'flarum/Component';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import classList from 'flarum/utils/classList';
import extractText from 'flarum/utils/extractText';
/**
* The `UserBio` component displays a user's bio, optionally letting the user
@ -30,7 +31,7 @@ export default class UserBio extends Component {
let content;
if (this.editing) {
content = <textarea className="FormControl" placeholder={app.trans('core.user_bio_placeholder')} rows="3" value={user.bio()}/>;
content = <textarea className="FormControl" placeholder={extractText(app.trans('core.user_bio_placeholder'))} rows="3" value={user.bio()}/>;
} else {
let subContent;