Single model for multiple views.

180 views
Skip to first unread message

gsb

unread,
Jul 6, 2012, 10:08:40 AM7/6/12
to agil...@googlegroups.com
What is the best way to "data-bind" model elements to actions for change in multiple views?

greg

Gav

unread,
Jul 6, 2012, 10:29:40 AM7/6/12
to agil...@googlegroups.com
Hi greg, 

To answer your original question, I would dispatch an event over a common event bus when the model changes, then any other agility objects who have views that also need to change would listen for that event from the common event bus and then update their own models to trigger their own bindings. 

Funnily enough i am just writing some documentation on an Agility project I recently done and have written the following on the common event bus, to hopefully this helps. 

###The common eventbus and Actors

The common event bus is simply an event dispatcher that all key classes (or
`Actors`) have access to.  An `Actor` could be an View a Service a Model or even
a small UI Component like a Button.

The idea is that any `Actor` can dispatch an event on the common eventbus and
because any other `Actor` has access to the common eventbus they can listen for
that event and react to it.

####Concrete implementation

This was implemented by creating an Agility object (or class) called the
`EventBus`. It literally did nothing, but I added it to the source code for
verbosity, but because it's an Agility object it is capable of dispatching
events and you can listen to events on it.

I never wanted anyone to directly use the `EventBus` if I could I would have
made it `internal` to the `Actor`.

The important class was the `Actor` which had access to the `EventBus`, and
exposed two important methods `dispatchOnBus()` and `addBusListener()`.  These
call the JQuery methods on the `EventBus` Agility object [.trigger()](http://api.jquery.com/trigger/) and
[.bind()](http://api.jquery.com/bind/) respectively.

It would have been nice to make `Actor` and abstract class, but even in AS3 we
don't have that feature in our language.

The concept of the common eventbus is best illustrated with a concrete example.
I have picked the `LoadingScreen` class for this. This class represents the
loading screen for the application, it's 100% wide and 100% high and fades out
when the application is ready to be interacted with.

It extends `DK.core.Actor`:

    DK.aas.LoadingScreen = $$(DK.core.Actor, {

It has an `onRegister()` method which is called by the `Actor` class when it is
created, this is a good place to add bus listeners:

    onRegister:function(){
        this.addBusListener(DK.aas.BootSequenceEvent.COMPLETE(),this.hide);
        ...
    },

The code above is called with the loading screen is created and states that when
the bootsequence event `COMPLETE` is dispatched, it will invoke the method
`hide()`.  Lets have a look at that method.

    hide:function(){
        this.view.$().fadeOut(500, this.onHideComplete);
    },

    onHideComplete:function(){
        this.dispatchOnBus(DK.aas.ScreenTransitionEvent.LOADING_SCREEN_HIDE_COMPLETE());
    },

The `hide()` method uses a JQuery fadeOut which lasts for 500ms which calls
`onHideComplete()` when it is finished.

`onHideComplete()` disaptches another event on the common eventbus,
`LOADING_SCREEN_HIDE_COMPLETE` which is listened to by the `AssetPickerScreen`.

Lets have a quick look at the `AssetPickerScreen` which also extends `Actor` and
therefore has access to the common eventbus.

    DK.aas.AssetPickerScreen = $$(DK.core.Actor, {

        ...

        onRegister:function(){
            this.addBusListener(DK.aas.ScreenTransitionEvent.LOADING_SCREEN_HIDE_COMPLETE(), this.show);
        },

        show:function(){
            this.view.$().fadeIn(500);
        },

I have edited the above snippet to only show the parts of interest, but here you
can see that it will listen for the `LOADING_SCREEN_HIDE_COMPLETE` and then
call it's own `show()` method which does a JQuery fade in.

The end result of the above is that once the bootsequence completes and the
application is ready the loading screen will transition away and once that
transition has completed the main screen, the asset picker screen, will
transition in - all of this has been achieved without the two screens knowing
about each other (other than events).

Tom Lackner

unread,
Jul 6, 2012, 12:08:43 PM7/6/12
to agil...@googlegroups.com
I wonder if it wouldn't be easier to just add an optional argument to $$() to cause it to listen to events triggered by all objects, not just triggered on it directly..

var btn = $$(true, {}, '<div><button>Cancel operation</button></div>', { 'persist:stop': function() { $(this).remove(); }});

t
--
Thomas Lackner * 305-978-8525

Gav

unread,
Jul 6, 2012, 12:18:22 PM7/6/12
to agil...@googlegroups.com
that's an interesting idea tom, its basically what I proposed, but built into the base Agility object taking advantage of the controller, it's probably a feature I would want enabled by default though.  So for Gregs original question, it would be really nice to be able to do:

var otherObject = $$({msg: ""},{},{})

var btn = $$({}, '<div><button>Cancel operation</button></div>', { 'change:otherObject.msg': function() { $(this).remove(); }}); 

I assume this is not possible at the moment right?

On Friday, 6 July 2012 17:08:43 UTC+1, Thomas Lackner wrote:
I wonder if it wouldn't be easier to just add an optional argument to $$() to cause it to listen to events triggered by all objects, not just triggered on it directly..

var btn = $$(true, {}, '<div><button>Cancel operation</button></div>', { 'persist:stop': function() { $(this).remove(); }});

t

Tom Lackner

unread,
Jul 6, 2012, 12:25:44 PM7/6/12
to agil...@googlegroups.com
Interesting idea to change the name of the event when it bubbles globally!

Remember there's no way to do wildcard matching (yet), and a common case is probably going to be to listen to ALL events, so it would probably be best to make it something easy and general:

var btn = $$({}, '<div><button>Cancel operation</button></div>', { 'global:change:msg': function() { $(this).remove(); }}); 

I proposed adding a method to turn it on/off because of performance implications: if your page has hundreds of objects (such as a news feed with a lot of items), and they're global by default, they'd all be sent every click message, but if only a few are registered as global listeners, we could quickly iterate through that list and send the 'global:...' event to only those. It would be very fast.

This isn't in the code now but it would be trivial to add, even by a noob like me. :) Here's the function we'd hack to bits:


t

gsb

unread,
Jul 6, 2012, 12:28:03 PM7/6/12
to agil...@googlegroups.com
Hey,  Sorry I have not kept up with the posts.

@Gav - thanks for a fast and detailed response, I appreciate your time.
@Thomas - You too, Thank you.  However I can not envission implementation of your suggestion.  I'll have to think on it.

Gavin,  I did something similar though not as formally as you.  Was trying to get enough time to show an example, but...

I shall continue and see where this thread goes.  Most importantly, I didn't want to re-invent the wheel only to find out that Agility supported a simple solution that I had missed.

Thanks.

Gav

unread,
Jul 6, 2012, 1:31:01 PM7/6/12
to agil...@googlegroups.com
@tom good point tom about performance implications. I reckon you'd still need to name space the event because you might have multiple objects with a model that contains a property called "msg" but i'm not sure how good the introspection is in Javascript (if anything even exists), but I do like the prefix of "global" because you are making your intentions nice and clear.

Not sure how trivial this is going to be to add, I guess i am more of a n00b than you :-) but it would be good if this worked because it would natively (to agility) solve the whole objects talking to each other thing.




gsb

unread,
Jul 16, 2012, 9:47:25 PM7/16/12
to agil...@googlegroups.com

Today I had time to re-look at my issue of “one model bound to multiple views.”

For me, the use case is trivial – persistent application state data including user modified and/or default application settings. Some of these data are used in multiple agility UI-view objects. Current page and total pages for instance; or theming data perhaps.

Anyway, I wanted agility to handle the data-binding where appropriate without undue application code for special cases.

Firstly, I hope that someone more experienced than I is considering this issue and able to incorporate a general solution into agility.js. Meantime I made a simple work around that seems to play well in my limited testing. I'll attach a file for anyone interested.

In short, I made a small change to agility.js, reluctantly. The change to agility was in the model's setter method 'set' to insure that the change events are only fired if there is an actual difference between the proposed new value and the current model stored value. The issue is a 'feedback loop' of continuous change events when simply setting the data via the model set and change event.

First I made an agility object called 'global_data' defining a model and a controller:

var global_data = $$({

model : { name : 'Greg Baker' },

controller : {

'change:name' : function(event,obj) {

obj1.model.set({'name':this.model.get('name')});

obj2.model.set({'name':this.model.get('name')});

}

}

});


The intent is that upon change of the global_data's name field, two other object models, obj1 and obj2, are also 'manually' updated. Not cool, but not too much extra wiring.

Obj1 and obj2 are identical. Both are simple agility objects that use the global_data object as a prototype. Their model is actually merged with a local copy of the global_data's model. Their view is an input field with echo display (plagiarized from the agility documentation examples.) The controller monitors the user 'keystrokes' to the input field and for each keystroke, forces a data change event. The change even propagates back to the global_data model where it is echoed back to both obj1 and obj2.


Here is that code fragment:

var obj1 = $$(global_data, {

model: { title : "Obj1's value: " },

view : {

format : '<p><input type="text" data-bind="name" ' +

'autocomplete="off" placeholder="Enter name." /> &nbsp; ' +

'<b><span data-bind="title"/></b>&nbsp; ' +

'<span data-bind="name"/>'

},

controller : {

'keyup input' : function(event) {

this.view.$('input').change();

if (event.which == 13) this.view.$('input').blur();

}

}

});


The second object is identical except it is named 'obj2'.

Now, where ever and when ever 'name' is updated, all references are updated. So we can modify the global_data's controller to save our persistent data knowing it will always be current.


Remember to modify a local copy of agility.js for testing the attached file. The necessary changes are detailed in the attached file at the bottom as an HTML comment. This is a quick summary:

...

else {

// Commented out the next line.

//$.extend(this.model._data, arg);

}

for (var key in arg) {

delete _clone[ key ];

// Check for 'actual' change in the value.

if ( this.model._data[key] != arg[key] ) {

this.model._data[key] = arg[key];

modified.push(key);

}

}

...

index.html
Reply all
Reply to author
Forward
0 new messages