I'm looking for an event that tells me when a surface has been rendered so I can call methods like surface.focus().
If I call focus immediately after I create the surface it doesn't work. If I call it in a timer after some arbitrary time I expect it to be rendered, it works. So there must be an event I can use.
For example if I create a widget that builds a bunch of surfaces inside a view, how do I know when that widget has been fully built and more importantly, when is it being rendered so I can set focus on an input surface?
Thanks
I marked johntraver's response as the answer, but I also wanted to include a complete working example for the InputSurface for people like me just learning famous. This code subclasses InputSurface so that the focus method will work.
Once the InputSurface is rendered it gains focus.
TextBox.js
define(function(require, exports, module) {
var InputSurface = require('famous/surfaces/InputSurface');
var EventHandler = require('famous/core/EventHandler');
function TextBox(options) {
InputSurface.apply(this, arguments);
this._superDeploy = InputSurface.prototype.deploy;
}
TextBox.prototype = Object.create(InputSurface.prototype);
TextBox.prototype.constructor = TextBox;
TextBox.prototype.deploy = function deploy(target) {
this.eventHandler.trigger('surface-has-rendered', this);
this._superDeploy(target);
};
module.exports = TextBox;
});
implementation
this.email = new TextBox({
size: [300, 40],
placeholder:'email'
});
var event_handler = new EventHandler();
event_handler.on('surface-has-rendered', function(control){
control.focus();
});
this.email.pipe(event_handler);
This is another case of when subclassing may be your easiest and most straight forward approach. In this example, Surface is subclassed and I am sure to grab the deploy function of the Surface and bind it to the MySurface instance for later use. Then when we override deploy later on, we can call super for the surface and not have to worry about altering core code. eventHandler is a property built into Surface, so that is used to send the render event.
An interesting test happened while making this example. If you refresh the code, and grunt pushes the changes to an unopened tab.. You event will not be fired until you open the tab again. Makes sense, but it was nice to see!
Here is what I did..
Good Luck!
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var StateModifier = require('famous/modifiers/StateModifier');
var EventHandler = require('famous/core/EventHandler')
function MySurface(options) {
Surface.apply(this, arguments);
this._superDeploy = Surface.prototype.deploy
}
MySurface.prototype = Object.create(Surface.prototype);
MySurface.prototype.constructor = MySurface;
MySurface.prototype.elementType = 'div';
MySurface.prototype.elementClass = 'famous-surface';
MySurface.prototype.deploy = function deploy(target) {
this._superDeploy(target);
this.eventHandler.trigger('surface-has-rendered', this);
};
var context = Engine.createContext();
var event_handler = new EventHandler();
event_handler.on('surface-has-rendered', function(data){
console.log("Hello Render!");
console.log(data);
})
var surface = new MySurface({
size: [200,200],
content: "Hello",
properties: {
color: 'white',
textAlign: 'center',
lineHeight: '200px',
backgroundColor: 'green'
}
});
surface.pipe(event_handler);
context.add(new StateModifier({origin:[0.5,0.5]})).add(surface);
I know this has been answered already, all be it a few months ago. I just thought that I would add that currently you can do this without subclassing as follows:
var textbox = new InputSurface({
size: [true,true],
placeholder: 'Text'
});
textbox.on('deploy', function() {
textbox.focus();
});
Related
I'm attempting to add a new schema type to my Apostrophe-CMS project. I'm essentially trying to use Fabric.js to add an image designer as part of the pieces editor modal. I have a piece type that stores the JSON generated by Fabric.js as a field, and want the editor to show up in place of the field (or as the field) when editing the piece itself.
I have this partially working (following the guide at https://docs.apostrophecms.org/howtos/custom-schema-field-types.html). However, I'm running into an issue. The populate method of addFieldType seems to get called before the actual fieldset is rendered on the page. This would normally be fine since most fields have basic (if any) logic applied to them at render time, but in this case Fabric.js seems to require the canvas element to be present on the page, otherwise it can't connect to it correctly and fails to load.
I noticed in https://github.com/apostrophecms/apostrophe/blob/b22af9320169bf3af26cd17642373ae1e331a990/lib/modules/apostrophe-schemas/public/js/user.js on line 80 that it seems to imply that there's a way to override afterPopulate in custom field types. I can't seem to get this to work - I've tried adding it similarly to populate (seen below) but it doesn't ever get called (which makes sense looking at line 44 of the same file, since the only thing that gets called at that point on fieldType is populate).
Is there a way to override afterPopulate from a custom field type, or is there some other way I should be handling this? Right now I'm essentially adding a 500ms timeout in populate before initializing Fabric, which does work, but seems incredibly hacky. Here's the code that I've added so far:
apos.define('fabric-editor', {
afterConstruct: function(self) {
self.addFieldType();
},
construct: function(self, options) {
self.addFieldType = function() {
apos.schemas.addFieldType({
name: 'fabric-editor',
afterPopulate: self.afterPopulate,
populate: self.populate,
convert: self.convert
});
};
self.afterPopulate = function($el, schema, object, callback) {
callback();
}
self.populate = function(object, name, $field, $el, field, callback) {
var $fieldset = apos.schemas.findFieldset($el, name);
callback();
setTimeout(function () {
var canvas = $fieldset.find('#fabric-canvas');
if (canvas.length > 0) {
self.fabricCanvas = new fabric.Canvas(canvas[0]);
}
fabric.Object.prototype.transparentCorners = false;
fabric.Object.prototype.cornerColor = 'blue';
fabric.Object.prototype.cornerStyle = 'circle';
self.addRectangle();
}, 500)
};
self.addRectangle = function (ev) {
var rect = new fabric.Rect({
left: 10,
top: 10,
fill: 'red',
width: 100,
height: 100
});
self.fabricCanvas.add(rect).renderAll().setActiveObject(rect);
}
}
});
self.afterPopulate belongs to the apostrophe-schemas module, so you would need to overwrite it in a project-level lib/modules/apostrophe-schemas/public/js/user.js file.
I don't think that a field's populate function is called before the field is on the page, however, since it passes the form element in as an argument.
Having this function I need to create a new instance of it. Everything works fine in JavaScript but how to I convert it to TypeScript?
function Calendar(selector, events) {
this.el = document.querySelector(selector);
this.events = events;
this.current = moment().date(1);
this.draw();
var current = document.querySelector('.today');
if(current) {
var self = this;
window.setTimeout(function() {
self.openDay(current);
}, 500);
}
}
var calendar = new Calendar('#calendar', data);
var calendar = new Calendar('#calendar', data);
It is true that anything that works in JavaScript will work in TypeScript, but that just means that the TypeScript compiler will output your JavaScript more or less untouched, possibly spitting out a bunch of warnings on the way. If you just ignore the errors, things will still work.
But assuming you want to leverage the power of TypeScript, you should start changing things. Let's start.
First, you should install the typings from Moment.js in your project, probably by running npm install moment from your project folder.
Then, I usually like to turn on all the --strictXXX compiler flags (I think you can just use --strict) to get the maximum number of warnings to ignore and/or fix.
Okay, now: the ES6/TypeScript idiom for a constructible thing is to use a class. Here's a look at some modifications I made, with some inline comments:
import * as moment from 'moment';
class Calendar {
// a Calendar has an el property which is a possibly null DOM element:
el: Element | null;
// a Calendar has a current property which is a Moment:
current: moment.Moment;
// a Calendar has an events property which is an array of Event:
events: Event[];
// the constructor function is what gets called when you do new Calendar()
// note that I assume selector is a string and events is an array of Event
constructor(selector: string, events: Event[]) {
this.el = document.querySelector(selector);
this.events = events;
this.current = moment().date(1);
this.draw();
var current = document.querySelector('.today');
if (current) {
var self = this;
window.setTimeout(function() {
self.openDay(current);
}, 500);
}
}
draw() {
// needs an implementation
}
openDay(day: Element | null) {
// needs an implementation
}
}
declare let data: Event[]; // need to define data
var calendar = new Calendar('#calendar', data);
You need to implement the draw() and openDay() methods which are presumably part of the Calendar.prototype. I put stubs for them in there. You also need to define data, which is (I'm guessing) an array of events (if it's something else you need to change the type of events.
If you look at the compiled JavaScript output from the above, you'll see that it's more or less the same as what you had. But now, of course, TypeScript is happy to let you call new Calendar(...).
There are more changes you can make, of course. For example, you can use parameter properties and remove the this.events = events; line. Or you can use property initializers and move the this.current = ... out of the constructor function and into the property declaration. Et cetera.
But this should hopefully be enough to get you started. Good luck!
I am having trouble understanding in what situations you would use custom events.
I mean the ones created by the CustomEvent constructor.
I understand the syntax itself, just not why it is useful. It would be nice if somebody could provide an example of real world application of custom events.
I use it (shameless plug) to raise "resize" events on div elements and then use a separate binding framework (aurelia) to listen to those events.
the explicit code example is:
var element = this.element; // some element
var erd = erd({ strategy: 'scroll' });
var widthOld = element.offsetWidth;
var heightOld = element.offsetHeight;
this.callback = () => {
var event = new CustomEvent("resize", {
detail: {
width: this.element.offsetWidth,
height: this.element.offsetHeight,
widthOld: widthOld,
heightOld: heightOld
}
});
element.dispatchEvent(event);
widthOld = this.element.offsetWidth;
heightOld = this.element.offsetHeight;
};
erd.listenTo(this.element, this.callback);
where erd is element-resize-detector that allows you to detect when any div changes shape.
Could some one please explain to me, why in d3-tip library (https://github.com/Caged/d3-tip), the object constructor looks like this:
d3.tip = function() {
var direction = d3_tip_direction,
offset = d3_tip_offset,
html = d3_tip_html,
node = initNode(),
svg = null,
point = null,
target = null
function tip(vis) {
svg = getSVGNode(vis)
point = svg.createSVGPoint()
document.body.appendChild(node)
}
tip.show = function() {
// some function
}
//...
return tip
}
To explain my confusion, I'm trying to replicate this library with different behaviour and can't make a decision, should I left this pattern as it is or change it into more standard creation function.
The function returns the tip function/object. at the bottom of the function it return tip; which is the new tip function.
by doing this the variables var direction = d3_tip_direction // etc are protected and are not accessible outside of the functions scope.
To understand further, you should look into design patterns, there is an excellent resource here
For some reason I cannot comprehend, events are not being listened to by my View. The model IS changing, but the view doesn't seem to acknowledge these changes. Here's my code.
var playerSet = 1;
var bone = function(){
var app = {};
app.BoardModel = Backbone.Model.extend({
defaults: function(){
return{
board:[0,0,0,0,0,0,0,0,0],
allDisabled: false,
p1Score: 0,
p2Score: 0
}
},
setSlot: function(slot, ct){
var b = this.get("board");
b[slot] = ct;
this.set("board", b);
console.log("CHANGED");
}
});
app.Board = new app.BoardModel;
app.BoardView = Backbone.View.extend({
el: $("#ttt-board"),
initialize: function(){
this.listenTo(app.Board, "change", this.renderBoard);
},
renderBoard: function(){
console.log("HELLO THERE");
}
});
var tictac = new app.BoardView;
app.Board.setSlot(0,1);
};
bone();
When I fire setSlot, the model does change as the console outputs CHANGED, however I never see the renderBoard function being called.
This is probably incredibly simple, but it eludes me.
Your problem is that you are only changing the internal components of the array object, not the attribute on your model. Even though you are manually calling set on the model, this is not an actual change and the set logic only triggers a change event if the equality check between the old and new values fails (which in your case it doesn't).
Since you are calling a custom function anyways, why not just use a custom event?
setSlot: function(slot, ct){
this.get("board")[slot] = ct;
this.trigger("custom:change:board", slot, ct);
}
Now listen for the custom event (or both) instead of just change.
initialize: function(){
this.listenTo(app.Board, "change custom:change:board", this.renderBoard);
}