How to include GreenSock plugin for ScrollMagic in a ReactJS environment? - reactjs

How do I include the Greensock plugin for ScrollMagic in my ReactJS project?
The authors of ScrollMagic made plugins to incorporate libraries like GreenSock and Velocity. These work great when you simply include them in your head of your html doc like so
<script type="text/javascript" src="js/lib/greensock/TweenMax.min.js"></script>
<script type="text/javascript" src="scrollmagic/uncompressed/ScrollMagic.js"></script>
<script type="text/javascript" src="scrollmagic/uncompressed/plugins/animation.gsap.js"></script>
But when you're in ReactJS, you don't import javascript resources like this. You actually have to import them through processes like the npm command, then declare them in your react project like
import ScrollMagic from "scrollmagic"
Although I was able to import ScrollMagic into react files and start using scrollmagic, I haven't been able to import the greensock plugins. There's no documentation on how to do this. I tried to hack things apart by taking segments of code in animation.gsap.js and pasting it into the node_modules/scrollmagic/scrollmagic.js file (which isn't a good idea to be editing these files), but it either breaks the webpack compiler or it breaks my project code.
How do I use the greensock plugin for scrollmagic in a react environment?

I was able to build a wrapper. At a high level, what I did was study the code of plugins/animation.gsap.js, pulled out those Scene extended properties that I needed, changed the name space, and have it augment the behaviour of ScrollMagic in a separate react class prior to exporting it.
Specifically, what I did was create a new file called ./ScrollMagic.js and pasted the following contents:
import ScrollMagic from 'scrollmagic';
import {TweenLite as Tween,TimelineMax as Timeline} from 'gsap';
ScrollMagic.Scene.addOption("tweenChanges", // name
false, // default
function (val) { // validation callback
return !!val;
});
ScrollMagic.Scene.extend(function () {
var Scene = this,
_tween;
var log = function () {
if (Scene._log) { // not available, when main source minified
Array.prototype.splice.call(arguments, 1, 0, "(animation.gsap)", "->");
Scene._log.apply(this, arguments);
}
};
// set listeners
Scene.on("progress.plugin_gsap", function () {
updateTweenProgress();
});
Scene.on("destroy.plugin_gsap", function (e) {
Scene.removeTween(e.reset);
});
/**
* Update the tween progress to current position.
* #private
*/
var updateTweenProgress = function () {
if (_tween) {
var
progress = Scene.progress(),
state = Scene.state();
if (_tween.repeat && _tween.repeat() === -1) {
// infinite loop, so not in relation to progress
if (state === 'DURING' && _tween.paused()) {
_tween.play();
} else if (state !== 'DURING' && !_tween.paused()) {
_tween.pause();
}
} else if (progress != _tween.progress()) { // do we even need to update the progress?
// no infinite loop - so should we just play or go to a specific point in time?
if (Scene.duration() === 0) {
// play the animation
if (progress > 0) { // play from 0 to 1
_tween.play();
} else { // play from 1 to 0
_tween.reverse();
}
} else {
// go to a specific point in time
if (Scene.tweenChanges() && _tween.tweenTo) {
// go smooth
_tween.tweenTo(progress * _tween.duration());
} else {
// just hard set it
_tween.progress(progress).pause();
}
}
}
}
};
/**
* Add a tween to the scene.
* If you want to add multiple tweens, add them into a GSAP Timeline object and supply it instead (see example below).
*
* If the scene has a duration, the tween's duration will be projected to the scroll distance of the scene, meaning its progress will be synced to scrollbar movement.
* For a scene with a duration of `0`, the tween will be triggered when scrolling forward past the scene's trigger position and reversed, when scrolling back.
* To gain better understanding, check out the [Simple Tweening example](../examples/basic/simple_tweening.html).
*
* Instead of supplying a tween this method can also be used as a shorthand for `TweenMax.to()` (see example below).
* #memberof! animation.GSAP#
*
* #example
* // add a single tween directly
* scene.setTween(TweenMax.to("obj"), 1, {x: 100});
*
* // add a single tween via variable
* var tween = TweenMax.to("obj"), 1, {x: 100};
* scene.setTween(tween);
*
* // add multiple tweens, wrapped in a timeline.
* var timeline = new TimelineMax();
* var tween1 = TweenMax.from("obj1", 1, {x: 100});
* var tween2 = TweenMax.to("obj2", 1, {y: 100});
* timeline
* .add(tween1)
* .add(tween2);
* scene.addTween(timeline);
*
* // short hand to add a TweenMax.to() tween
* scene.setTween("obj3", 0.5, {y: 100});
*
* // short hand to add a TweenMax.to() tween for 1 second
* // this is useful, when the scene has a duration and the tween duration isn't important anyway
* scene.setTween("obj3", {y: 100});
*
* #param {(object|string)} TweenObject - A TweenMax, TweenLite, TimelineMax or TimelineLite object that should be animated in the scene. Can also be a Dom Element or Selector, when using direct tween definition (see examples).
* #param {(number|object)} duration - A duration for the tween, or tween parameters. If an object containing parameters are supplied, a default duration of 1 will be used.
* #param {object} params - The parameters for the tween
* #returns {Scene} Parent object for chaining.
*/
Scene.setTween = function (TweenObject, duration, params) {
var newTween;
if (arguments.length > 1) {
if (arguments.length < 3) {
params = duration;
duration = 1;
}
TweenObject = Tween.to(TweenObject, duration, params);
}
try {
// wrap Tween into a Timeline Object if available to include delay and repeats in the duration and standardize methods.
if (Timeline) {
newTween = new Timeline({
smoothChildTiming: true
}).add(TweenObject);
} else {
newTween = TweenObject;
}
newTween.pause();
} catch (e) {
log(1, "ERROR calling method 'setTween()': Supplied argument is not a valid TweenObject");
return Scene;
}
if (_tween) { // kill old tween?
Scene.removeTween();
}
_tween = newTween;
// some properties need to be transferred it to the wrapper, otherwise they would get lost.
if (TweenObject.repeat && TweenObject.repeat() === -1) { // TweenMax or TimelineMax Object?
_tween.repeat(-1);
_tween.yoyo(TweenObject.yoyo());
}
// Some tween validations and debugging helpers
if (Scene.tweenChanges() && !_tween.tweenTo) {
log(2, "WARNING: tweenChanges will only work if the TimelineMax object is available for ScrollMagic.");
}
// check if there are position tweens defined for the trigger and warn about it :)
if (_tween && Scene.controller() && Scene.triggerElement() && Scene.loglevel() >= 2) { // controller is needed to know scroll direction.
var
triggerTweens = Tween.getTweensOf(Scene.triggerElement()),
vertical = Scene.controller().info("vertical");
triggerTweens.forEach(function (value, index) {
var
tweenvars = value.vars.css || value.vars,
condition = vertical ? (tweenvars.top !== undefined || tweenvars.bottom !== undefined) : (tweenvars.left !== undefined || tweenvars.right !== undefined);
if (condition) {
log(2, "WARNING: Tweening the position of the trigger element affects the scene timing and should be avoided!");
return false;
}
});
}
// warn about tween overwrites, when an element is tweened multiple times
if (parseFloat(TweenLite.version) >= 1.14) { // onOverwrite only present since GSAP v1.14.0
var
list = _tween.getChildren ? _tween.getChildren(true, true, false) : [_tween],
// get all nested tween objects
newCallback = function () {
log(2, "WARNING: tween was overwritten by another. To learn how to avoid this issue see here: https://github.com/janpaepke/ScrollMagic/wiki/WARNING:-tween-was-overwritten-by-another");
};
for (var i = 0, thisTween, oldCallback; i < list.length; i++) { /*jshint loopfunc: true */
thisTween = list[i];
if (oldCallback !== newCallback) { // if tweens is added more than once
oldCallback = thisTween.vars.onOverwrite;
thisTween.vars.onOverwrite = function () {
if (oldCallback) {
oldCallback.apply(this, arguments);
}
newCallback.apply(this, arguments);
};
}
}
}
log(3, "added tween");
updateTweenProgress();
return Scene;
};
/**
* Remove the tween from the scene.
* This will terminate the control of the Scene over the tween.
*
* Using the reset option you can decide if the tween should remain in the current state or be rewound to set the target elements back to the state they were in before the tween was added to the scene.
* #memberof! animation.GSAP#
*
* #example
* // remove the tween from the scene without resetting it
* scene.removeTween();
*
* // remove the tween from the scene and reset it to initial position
* scene.removeTween(true);
*
* #param {boolean} [reset=false] - If `true` the tween will be reset to its initial values.
* #returns {Scene} Parent object for chaining.
*/
Scene.removeTween = function (reset) {
if (_tween) {
if (reset) {
_tween.progress(0).pause();
}
_tween.kill();
_tween = undefined;
log(3, "removed tween (reset: " + (reset ? "true" : "false") + ")");
}
return Scene;
};
});
export default ScrollMagic;
You will notice that this looks almost exactly like plugins/animation.gsap.js, with the exceptions being:
I added my own import lines at the top, so make sure you have npm installed scrollmagic and gsap ahead of time
I export the ScrollMagic object towards the end
plugins/animation.gsap.js had some other code that turn the plugin into a factory which I excluded because it doesn't apply here
Now I can use .setTween() in my react project. Example usage:
import React, {Component} from "react";
import ScrollMagic from "./ScrollMagic"; // my own wrapper for scrollmagic that includes greensock
export default class Home extends Component {
componentDidMount()
{
var controller = new ScrollMagic.Controller();
var item = "#whateverstuffselector";
var scene = new ScrollMagic.Scene({triggerElement:item})
.setTween(item, 0.5, {backgroundColor: "red", scale: 3})
.addTo(controller);
}
render()
{
return (<div id="whateverstuffselector">stuff</div>);
}
}

After escalating this issue for 1 month I guess I found great solution.
So this issue shows that in React environment we can not get animation.gsap file.
This fix does not require any webpack changes except animation.gsap file itself.
Find these files in "node_module" directory tree (may have different location on you PC) and import it in this way to your working JS file (App.js for example).
import "../../node_modules/scrollmagic/scrollmagic/uncompressed/plugins/animation.gsap";
import "../../node_modules/scrollmagic/scrollmagic/uncompressed/plugins/debug.addIndicators";
Go to animation.gsap and add these two lines of code at the beginning of file.
import { TimelineMax, TweenMax, TweenLite} from "gsap/all";
import ScrollMagic from "scrollmagic";
Go to debug.addIndicators and add this line of code at the beginning of file (in case if you need indicator debugger, but I strongly suggest not to skip this step).
import ScrollMagic from "scrollmagic";
In animation.gsap find first function and delete all "root" variables and change them to variables that I provided below. ( you should find 8 of them).
Before:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic', 'TweenMax', 'TimelineMax'], factory);
} else if (typeof exports === 'object') {
// CommonJS
// Loads whole gsap package onto global scope.
require('gsap');
factory(require('scrollmagic'), TweenMax, TimelineMax);
} else {
// Browser globals
factory(root.ScrollMagic || (root.jQuery && root.jQuery.ScrollMagic), root.TweenMax || root.TweenLite, root.TimelineMax || root.TimelineLite);
}
}
After:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic', 'TweenMax', 'TimelineMax'], factory);
} else if (typeof exports === 'object') {
// CommonJS
// Loads whole gsap package onto global scope.
require('gsap');
factory(require('scrollmagic'), TweenMax, TimelineMax);
} else {
// Browser globals
factory(ScrollMagic || (jQuery && jQuery.ScrollMagic), TweenMax || TweenLite, TimelineMax || TimelineLite);
}
}
In debug.addIndicators also delete all "root" variables ( you should find 4 of them ) and change them to variables that I provided below.
Before:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic'], factory);
} else if (typeof exports === 'object') {
// CommonJS
factory(require('scrollmagic'));
} else {
// no browser global export needed, just execute
factory(root.ScrollMagic || (root.jQuery && root.jQuery.ScrollMagic));
}
}
After:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic'], factory);
} else if (typeof exports === 'object') {
// CommonJS
factory(require('scrollmagic'));
} else {
// no browser global export needed, just execute
factory(ScrollMagic || (jQuery && jQuery.ScrollMagic));
}
}
I hope this solution will work for you.
In any case you can reach me for help.

Related

How can we override Ext.Base?

I am using Ext JS v7.1 and I have overridden Ext.Base to set my naming scheme for the classes that inherits from Ext.Base: This eases my debugging.
Ext.define('App.class.Base', {
override: 'Ext.Base',
constructor: function() {
var me = this
/**
* App.base.store.Base => store-base-
* App.store.Menu => store-menu-
*/
if (me.isIdentifiable) {
if (!me.self.prototype.hasOwnProperty('identifiablePrefix')) {
const classNameParts = me.$className.match(/([^\.]+)/g)
if (classNameParts && classNameParts[0] === 'App') {
classNameParts.splice(0, classNameParts.length - 2)
me.self.prototype.identifiablePrefix = classNameParts.reduce((i, j) => i + '-' + j).toLocaleLowerCase() + '-'
}
}
}
return me.callParent()
}
})
This code was building before without an error but, after I upgraded Sencha Cmd to v7.3.0.19, I started the get the following error:
[ERR] C2016: Override target not found -- /...../packages/local/module-core/overrides/class/Base.js:2:64
[WRN] Override App.class.Base in file /..../packages/local/module-core/overrides/class/Base.js had no target detected
I don't know whether this is the right place/way to do this override, if not I can change my implementation. However, if there is no other way, how can get rid of the build error?
Thanks in advance,
Ipek
Because i am not using sencha build tools anymore, i can not help you directly but i would like to share another approach:
In case you have loaded the framework (ext-debug-all or ext-all, etc.) first and the class which should get overwritten is already defined you can do it like that:
Ext.Component.override({
initComponent: function () {
Ext.log('bootstraping ' + this.self.getName());
var me = this,
width = me.width,
height = me.height;
// If plugins have been added by a subclass's initComponent before calling up to here (or any components
// that don't have a table view), the processed flag will not have been set, and we must process them again.
// We could just call getPlugins here however most components don't have them so prevent the extra function call.
if (me.plugins && !me.plugins.processed) {
me.plugins = me.constructPlugins();
}
me.pluginsInitialized = true;
// this will properly (ignore or) constrain the configured width/height to their
// min/max values for consistency.
if (width != null || height != null) {
me.setSize(width, height);
}
if (me.listeners) {
me.on(me.listeners);
me.listeners = null; //change the value to remove any on prototype
}
if (me.focusable) {
me.initFocusable();
}
}
});
Depending on the further internal processing you can call callParent or callSuper.
More details here:
https://docs.sencha.com/extjs/6.5.3/classic/Ext.Class.html#cfg-override
You may be able to move this upper code inside a function and call it later, for example - when Ext.isReady. I guess this can solve or tackle some of the open tooling issues you are facing.
UPDATE:
Coming back to your question you can do the following and define it like that:
Ext.Base.override({
constructor: function() {
var me = this
/**
* App.base.store.Base => store-base-
* App.store.Menu => store-menu-
*/
if (me.isIdentifiable) {
if (!me.self.prototype.hasOwnProperty('identifiablePrefix')) {
const classNameParts = me.$className.match(/([^\.]+)/g)
if (classNameParts && classNameParts[0] === 'App') {
classNameParts.splice(0, classNameParts.length - 2)
me.self.prototype.identifiablePrefix = classNameParts.reduce((i, j) => i + '-' + j).toLocaleLowerCase() + '-'
}
}else{
console.log('isIdentifiable');
console.log(me.identifiablePrefix);
}
}
return me.callParent(arguments)
}
});
I have added an exampole fiddle here. It should log "helloWorld" in case identifiablePrefix is set.
https://fiddle.sencha.com/#view/editor&fiddle/3a8i

Best practice to create reusable and decoupled React components in 18n/l10n apps with custom formatting

I have the following React component which I would like to reuse in several applications:
import React from "react";
import { useFactory } from "react-js-utl/hooks";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
/**
* Shows current account balance with variation since previous balance.
*/
const Balance = compose(React.memo)(function Balance({
current,
previous,
label,
sinceLabel,
increaseColor = "light-green",
increaseIcon = "arrow-up",
decreaseColor = "red",
decreaseIcon = "arrow-down",
sameColor = "blue",
sameIcon = "equals",
formatNum = void 0,
formatPerc = void 0
} = {}) {
// Compute the change.
let change = ((current - previous) / previous) * 100;
// Implementation detail. Switch objects depeding on runtime value of change.
const outcomeFactory = useFactory(
() => [
// Increase:
[
change > 0, // Condition, if true, the object following it will be returned by `useFactory()`.
{
outcome: "increase",
color: increaseColor,
icon: increaseIcon
}
],
// Decrease:
[
change < 0,
{
outcome: "decrease",
color: decreaseColor,
icon: decreaseIcon
}
],
// Same (default, returned if the previous conditions evaluate to false):
{
outcome: "same",
color: sameColor,
icon: sameIcon
}
],
// deps array
[
change,
increaseColor,
increaseIcon,
decreaseColor,
decreaseIcon,
sameColor,
sameIcon
]
);
change = Math.abs(change);
return (
<div className="balance">
<div className="balance-label">{label}</div>
<div className="balance-current">
{formatNum ? formatNum(current) : current}
</div>
<span
className={`balance-outcome balance-outcome-${outcomeFactory.outcome} balance-outcome-${outcomeFactory.color}`}
>
{typeof outcomeFactory.icon === "string" ? (
// Defaults to FontAwesome's icon.
<FontAwesomeIcon icon={outcomeFactory.icon} />
) : (
// Custom component rendered by client code.
outcomeFactory.icon
)}
{outcomeFactory.outcome === "same"
? // Same (no change since previous):
""
: // Increase or decrease since previous:
// Edge case: previous balance was zero
Number(previous) === 0
? ""
: // Percentage increase/decrease:
formatPerc
? formatPerc(change)
: change}
</span>
<span className="balance-since-label">{sinceLabel}</span>
</div>
);
});
Balance.displayName = "Balance";
export default Balance;
Which I use in an app like this:
...
function myFormattingFunctionForNumbers(t, num) {
// I use accounting.js to format numbers:
return accounting.format(num, {
decimal: t("formatting:numbers.decimal_separator"), // For EN lang this will be "."
thousand: t("formatting:numbers.thousand_separator") // For EN this this will be ","
})
}
function myFormattingFunctionForPercentages(t, perc) {
return myFormattingFunctionForNumbers(t, perc) + "%";
}
...
// Inside app component:
// Using react-i18next:
const { t, i18n } = useTranslation();
// If the language changes, so does formatting:
formatNum = useCallback(current => myFormattingFunctionForNumbers(t, current), [t, i18n.language])
formatPerc = useCallback(changePerc => myFormattingFunctionForPercentages(t, changePerc), [t, i18n.language])
...
<Balance
current={11234.56} // These hardcoded values could be props, of course.
previous={9321.45}
label={t("Your account balance")} // i18n
sinceLabel={t("Since previous month")} // i18n
formatNum={formatNum} // Function to format current value.
formatPerc={formatPerc} // Function to format change/variation percentage.
/>
...
Which outputs something looking roughly like this:
Your account balance
11,234.56
↑ 20,52% Since previous month
Right now I am facing the following "issues":
The reusable component is pure (uses React.memo), so its formatting functions formatNum and formatPerc need to change when the app's language changes even if the other props like current and previous don't change because a different language potentially involves different formatting and therefore the component should rerender;
Because of point 1, the client is responsible of wiring all the formatting functions inside the consuming component using useCallback which creates lot of biolerplate...;
The two useCallbacks do not warn me that I need to pass i18n.language to the deps array, simply because the current language is not referenced directly by the formatting functions myFormattingFunctionForNumbers and myFormattingFunctionForPercentages, which only use the t function of i18next (which as far as I know does not change when the language changes);
Maybe there is a point 4 and even a point 5, of which I am not aware of for now.
What is the current best practice for reusable React components which support formatting and i18n/l10n?
Tips and tricks on how to organize formatting/i18n/l10n code and separate these concerns from reusable components will be appreciated.
Thank you for the attention.

mousemove event, access to this context

Fisrt of all I'm using D3js inside a React component so I use some variable of my class to save datas e.g.: this.graphicalId = 'test';
I have two items in my d3 element, svgViewport which is a g element and streams which are path elements. I have .on('mousemove' event handle for each.
In the streams event I would like save the name of the current stream using d3.select(this) (note I'm in a function() and not an arrow function so this is local) in a global variable in order to use it in the svgViewport event.
My problem is that like I'm in a function() this is local and not link to my class instance so I can't save the value in a member variable this.currentStreamName.
A bit of code :
svgViewport.on('mousemove', function (d, i) {
if (mouseIsOverStream) {
let mousex = d3.mouse(this);
mousex = mousex[0];
// here I want this of the class instance context
this.nearestTickPosition, this.currentStreamName = findNearestTickPosition(mousex);
}
});
Do you have some advices to deal with it ?
Thanks.
You can use arrow functions to get access to the instance's this context and still acquire the current DOM element. For the DOM element you resort to the little-known and often overlooked third parameter of the event listener. As the docs have it (emphasis mine):
the specified listener will be evaluated for the element, being passed the current datum (d), the current index (i), and the current group (nodes)
Since the current index i is the pointer into the current group nodes you can refer to the current DOM element as nodes[i].
Your code thus becomes:
svgViewport.on('mousemove', (d, i, nodes) => {
if (mouseIsOverStream) {
let mousex = d3.mouse(nodes[i]); // get the current element as nodes[i]
mousex = mousex[0];
// this now refers to your instance
this.nearestTickPosition, this.currentStreamName = findNearestTickPosition(mousex);
}
});
Store the class context in a variable out of the event binding. Then, make an IIFE and bind it's context to the stored one.
componentDidMount() {
const ctx = this;
svgViewport.on('mousemove', function (d, i) {
if (mouseIsOverStream) {
let mousex = d3.mouse(this);
mousex = mousex[0];
!function () {
// here I want this of the class instance context
this.nearestTickPosition, this.currentStreamName = findNearestTickPosition(mousex);
}.bind(ctx)();
}
});
}
Also, this should work too:
componentDidMount() {
svgViewport = ...;
svgViewport.on('mousemove', (d, i) => {
if (mouseIsOverStream) {
let mousex = d3.mouse(svgViewport); // here
mousex = mousex[0];
this.nearestTickPosition, this.currentStreamName = findNearestTickPosition(mousex);
}
});
}

What's a nice pattern for ExtJS View Component to have own private Controller?

I've some ExtJS 4 MVC apps that share certain custom view components. A shared component has its own namespace and is often sufficiently complex (e.g. with a large tree of descendant components that must be managed) that it warrants its own controller. I want this 'component specific controller' to be properly encapsulated (private) within the component (i.e. nothing outside the component should have to know or care that the component is using an embedded controller). Of course, it should be possible for an app to have multiple instances of the component (each encapsulating a separate instance of its component controller).
To this end, I've developed the following mixin (this version for ExtJS 4.1), but I'm keen to know if anyone has solved the same problem more elegantly:
/**
* A mixin that provides 'controller' style facilities to view components.
*
* This is for 'view' components that also serve as the 'controller' for the component.
* In such situations where an embedded controller is required we would have preferred to use a separate
* Ext.app.Controller derived controller but it looks like Ext.app.Controllers are global things,
* i.e. their refs can't be scoped to within the component (allowing multiple instances on the component to be used,
* each with their own controller each with its own set of refs).
*
* Usage:
* - Declare a 'refs' config just as in an Ext.app.Controller.
* - Call this.setupControllerRefs() from your initComponent() template function.
* - Call this.control(String/Object selectors, Object listeners) just as you would in a Ext.app.Controller.init()
* template function.
* - Any events fired from within a Window need special treatment, because Ext creates Windows as floated
* (top-level) components, so our usual selector scoping technique doesn't work. The trick is to give each Window
* an itemId prefixed with this component's itemId, e.g. itemId: me.itemId + '-lookup-window'
* Then, in the 'this.control({...})' block, define selectors as necessary that begin with "#{thisItemId}-", e.g.
* '#{thisItemId}-lookup-window aux-filter-criteria': ...
*
* It is also recommended to keep the 'view' aspect of the component minimal. If there is a significant proportion of
* view code, push it down into a new component class. Ideally, the component/controller should be just a Container.
*/
Ext.define('Acme.CmpController', {
setupControllerRefs: function() {
var me = this,
refs = me.refs;
// Copied from Ext.app.Controller.ref
refs = Ext.Array.from(refs);
Ext.Array.each(refs, function(info) {
var ref = info.ref,
fn = 'get' + Ext.String.capitalize(ref);
if (!me[fn]) {
me[fn] = Ext.Function.pass(me.getRef, [ref, info], me);
}
});
},
/** #private (copied from Ext.app.Controller.ref) */
getRef: function(ref, info, config) {
this.refCache = this.refCache || {};
info = info || {};
config = config || {};
Ext.apply(info, config);
if (info.forceCreate) {
return Ext.ComponentManager.create(info, 'component');
}
var me = this,
selector = info.selector,
cached = me.refCache[ref];
if (!cached) {
//me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0];
/**** ACME ****/ me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector, this)[0];
if (!cached && info.autoCreate) {
me.refCache[ref] = cached = Ext.ComponentManager.create(info, 'component');
}
if (cached) {
cached.on('beforedestroy', function() {
me.refCache[ref] = null;
});
}
}
return cached;
},
control: function(selectors, listeners) {
var me = this,
selectorPrefix,
thisIemIdPrefix = '#{thisItemId}',
newSelectors = {};
if (listeners)
throw "Support for the optional 'listeners' param (which we had thought was rarely used) has not yet been coded.";
// Since there could be multiple instances of the controller/component, each selector needs to be
// prefixed with something that scopes the query to within this component. Ensure each instance has
// an itemId, and use this as the basis for scoped selectors.
me.itemId = me.itemId || me.id;
if (!me.itemId)
throw "We assume the component will always have an 'id' by the time control() is called.";
selectorPrefix = '#' + me.itemId + ' ';
Ext.Object.each(selectors, function(selector, listeners) {
if (selector.indexOf(thisIemIdPrefix) === 0)
selector = '#' + me.itemId + selector.substring(thisIemIdPrefix.length);
else
selector = selectorPrefix + selector;
newSelectors[selector] = listeners;
});
selectors = newSelectors;
// Real Controllers use the EventBus, so let's do likewise.
// Note: this depends on a hacked EventBus ctor. See ext-fixes.js
Ext.app.EventBus.theInstance.control(selectors, listeners, me);
}
});
As referred to in that last comment, ExtJS must be patched as follows:
Ext.override(Ext.app.EventBus, {
/**
* Our CmpController mixin needs to get a handle on the EventBus, as created by the Ext.app.Application instance. Analysis
* of the ExtJS source code shows that only one instance of EventBus gets created (assuming there's never more than one
* Ext.app.Application per app). So we hack the ctor to store a reference to itself as a static 'theInstance' property.
*/
constructor: function() {
this.callOverridden();
/**** ACME ****/ this.self.theInstance = this;
},
/**
* Had to patch this routine on the line labelled **** ACME ****. Events intercepted by Pv were being received by the Pv
* instance first created by appPv. appPv created a new Pv instance every time a 'to.AcmeViewer.View' message is received.
* Even though the old Pv had isDestroyed:true, the routine below was dispatching the event to it.
*
* It's possible this surprising behaviour is not unconnected with our (mis?)use of EventBus in Acme.CmpController.
*
* This patched function is from ExtJS 4.1.1
*/
dispatch: function(ev, target, args) {
var bus = this.bus,
selectors = bus[ev],
selector, controllers, id, events, event, i, ln;
if (selectors) {
// Loop over all the selectors that are bound to this event
for (selector in selectors) {
// Check if the target matches the selector
if (selectors.hasOwnProperty(selector) && target.is(selector)) {
// Loop over all the controllers that are bound to this selector
controllers = selectors[selector];
for (id in controllers) {
if (controllers.hasOwnProperty(id)) {
// Loop over all the events that are bound to this selector on this controller
events = controllers[id];
for (i = 0, ln = events.length; i < ln; i++) {
event = events[i];
/**** ACME ****/ if (!event.observable.isDestroyed)
// Fire the event!
if (event.fire.apply(event, Array.prototype.slice.call(args, 1)) === false) {
return false;
}
}
}
}
}
}
}
return true;
}
});
Although I'm currently on ExtJS 4.1 I'm also interested to hear about solutions that depend on 4.2, as this may help motivate me to migrate.

Back button using Extjs 3.4 History with ie8 & ie9 standards document mode

I am having a problem with Ext.History utility (version 3.4.0) working properly in IE8+. It works in Quirks mode, but not with the document mode in IE8 Standards mode (IE8) or IE9 Standards mode (IE9). Quirks mode isn't working for us because it is not rendering our CSS properly.
I have stripped everything out of the app except the history utility and now have two files (besides the extjs files):
index.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<html>
<head>
<script type="text/javascript" src="ext-base.js"></script>
<script type="text/javascript" src="ext-all.js"></script>
<script type="text/javascript" src="myapp.js"></script>
</head>
<body>
<div>
<div align="center">
<table width="97%" border="0" cellpadding="0" cellspacing="0" >
<tr>
<td>
Link1 |
Link2 |
Link3 |
Link4 |
link5
</td>
</tr>
</table>
</div>
</div>
<!-- Fields required for history management -->
<form id="history-form" class="x-hidden">
<input type="hidden" id="x-history-field"/>
<iframe id="x-history-frame"></iframe>
</form>
</body>
</html>
myapp.js:
Ext.ns('MyApp');
Ext.onReady(function()
{
Ext.History.init();
Ext.History.on('change', function(token){}, this);
});
When I load the app in a web server, go to index.html, and click link1, the address bar shows #link1. I then click link2 and the address bar shows #link2. I then click link3 and the address bar shows #link3.
Using the back button in IE with IE7 Emulation, in Chrome or in Firefox, the address bar will go from #link3 to #link2. When I hit the back button a second time, the address bar goes from #link2 to #link1. This is the behavior I expect.
However, using IE8 or IE 9 in the appropriate document standards mode, when I click the back button a second time, the address bar goes from #link2 back to #link3. Further clicks of the back button will just toggle the user between #link2 and #link3. This behavior is unexpected and is causing our application not to work properly.
Note that this is the way that the Sencha example works for 3.4.0:
Sencha 3.4 Sample
(the page renders in Quirks mode, but if you change it to IE8 Standards or IE9 Standards, it doesn't work).
It does work seem to work properly in 4.1:
(only let me posts 2 links, but you can probably find it...)
I don't have access to Ext 3.4.1, but this issue isn't listed as in the bug fixes. I have seen one thread (here) that suggests that changing the doctype would work, but that doesn't seem to be the case (I have tried all the doctypes...).
Note that many parts of our app are using the History utility for navigation, so removing it is not an acceptable solution.
Can anyone offer any suggestions on how I can get this to work?
This was actually pretty straightforward. I downloaded Ext 4.1 and looked at what they were doing with the Ext.util.History class. They define a variable for oldIEMode and use that for all the conditionals where in 3.4 they are using Ext.isIE.
So I edited the Ext.History class in ext-all-debug.js and defined the following variable at the top:
var oldIEMode = Ext.isIE6 || Ext.isIE7 || !Ext.isStrict && Ext.isIE8;
There were three conditionals in the class that were checking for Ext.isIE which I replaced with oldIEMode.
I rebuilt and deployed the app and the issue was resolved.
Editing ext-all.js is not best practice, but I should be able to overwrite this class instead.
This is how I ended up solving it:
I created a new patch javascript file and included it after the ext files
/*
#Author: RWR 20130224
This fixes the issue with backward traversal of history in IE8 & higher in standard document mode.
This class was challenging to override (http://www.sencha.com/forum/showthread.php?46306-Help-How-to-extend-Ext.History)
I ended up pasting all of the source original code here and making the necessary changes.
NOTE that this may be patched in version 3.4.1. It is definitely patched in 4.1. When upgrading, validate that this patch is still required.
*/
NewHistory = (function () {
var iframe, hiddenField;
var ready = false;
var currentToken;
var oldIEMode = Ext.isIE6 || Ext.isIE7 || !Ext.isStrict && Ext.isIE8;
function getHash() {
var href = location.href, i = href.indexOf("#"),
hash = i >= 0 ? href.substr(i + 1) : null;
if (Ext.isGecko) {
hash = decodeURIComponent(hash);
}
return hash;
}
function doSave() {
hiddenField.value = currentToken;
}
function handleStateChange(token) {
currentToken = token;
Ext.History.fireEvent('change', token);
}
function updateIFrame (token) {
var html = ['<html><body><div id="state">',Ext.util.Format.htmlEncode(token),'</div></body></html>'].join('');
try {
var doc = iframe.contentWindow.document;
doc.open();
doc.write(html);
doc.close();
return true;
} catch (e) {
return false;
}
}
function checkIFrame() {
if (!iframe.contentWindow || !iframe.contentWindow.document) {
setTimeout(checkIFrame, 10);
return;
}
var doc = iframe.contentWindow.document;
var elem = doc.getElementById("state");
var token = elem ? elem.innerText : null;
var hash = getHash();
setInterval(function () {
doc = iframe.contentWindow.document;
elem = doc.getElementById("state");
var newtoken = elem ? elem.innerText : null;
var newHash = getHash();
if (newtoken !== token) {
token = newtoken;
handleStateChange(token);
location.hash = token;
hash = token;
doSave();
} else if (newHash !== hash) {
hash = newHash;
updateIFrame(newHash);
}
}, 50);
ready = true;
Ext.History.fireEvent('ready', Ext.History);
}
function startUp() {
currentToken = hiddenField.value ? hiddenField.value : getHash();
if (oldIEMode) {
checkIFrame();
} else {
var hash = getHash();
setInterval(function () {
var newHash = getHash();
if (newHash !== hash) {
hash = newHash;
handleStateChange(hash);
doSave();
}
}, 50);
ready = true;
Ext.History.fireEvent('ready', Ext.History);
}
}
return {
/**
* The id of the hidden field required for storing the current history token.
* #type String
* #property s
*/
fieldId: 'x-history-field',
/**
* The id of the iframe required by IE to manage the history stack.
* #type String
* #property s
*/
iframeId: 'x-history-frame',
events:{},
/**
* Initialize the global History instance.
* #param {Boolean} onReady (optional) A callback function that will be called once the history
* component is fully initialized.
* #param {Object} scope (optional) The scope (<code>this</code> reference) in which the callback is executed. Defaults to the browser window.
*/
init: function (onReady, scope) {
if(ready) {
Ext.callback(onReady, scope, [this]);
return;
}
if(!Ext.isReady){
Ext.onReady(function(){
Ext.History.init(onReady, scope);
});
return;
}
hiddenField = Ext.getDom(Ext.History.fieldId);
if (oldIEMode) {
iframe = Ext.getDom(Ext.History.iframeId);
}
this.addEvents(
/**
* #event ready
* Fires when the Ext.History singleton has been initialized and is ready for use.
* #param {Ext.History} The Ext.History singleton.
*/
'ready',
/**
* #event change
* Fires when navigation back or forwards within the local page's history occurs.
* #param {String} token An identifier associated with the page state at that point in its history.
*/
'change'
);
if(onReady){
this.on('ready', onReady, scope, {single:true});
}
startUp();
},
/**
* Add a new token to the history stack. This can be any arbitrary value, although it would
* commonly be the concatenation of a component id and another id marking the specifc history
* state of that component. Example usage:
* <pre><code>
// Handle tab changes on a TabPanel
tabPanel.on('tabchange', function(tabPanel, tab){
Ext.History.add(tabPanel.id + ':' + tab.id);
});
</code></pre>
* #param {String} token The value that defines a particular application-specific history state
* #param {Boolean} preventDuplicates When true, if the passed token matches the current token
* it will not save a new history step. Set to false if the same state can be saved more than once
* at the same history stack location (defaults to true).
*/
add: function (token, preventDup) {
if(preventDup !== false){
if(this.getToken() == token){
return true;
}
}
if (oldIEMode) {
return updateIFrame(token);
} else {
location.hash = token;
return true;
}
},
/**
* Programmatically steps back one step in browser history (equivalent to the user pressing the Back button).
*/
back: function(){
history.go(-1);
},
/**
* Programmatically steps forward one step in browser history (equivalent to the user pressing the Forward button).
*/
forward: function(){
history.go(1);
},
/**
* Retrieves the currently-active history token.
* #return {String} The token
*/
getToken: function() {
return ready ? currentToken : getHash();
}
};
})();
Ext.apply(NewHistory, new Ext.util.Observable());
Ext.apply(Ext.History, NewHistory);

Resources