MVC Bindings Between Collection.selection and ModelController.model

edited October 2013 in Enyo 2.4
I have two panels, one that contains a DataList, and the other that's backed by a ModelController. I'm trying to get the selected item in the DataList to bind to the ModelController... and more importantly to get the data from the ModelController to propagate to the View. This is a standard list where you pick and item from the list to focus on it...

Problem 1: I couldn't get the DataList selection to automatically bind, so I ended up manually doing it in an event block for the list :

var selectedItem = this.selected();
this.app.controllers.message.set("model", selectedItem);
Problem 2: I can't get my view to bind to the model found in this.app.controllers.message.model. This represents the latest of 100 combinations trying to get it to bind...

{name: "message", bindFrom: "model", bindingDefaults: {from: ".model", to: ".content"}, components: [
{content: "Activity Date:"},
{bindings: [{from: ".model.activityDate", to: ".content"}]},
{bindings: [{from: ".activityDate", to: ".content"}]},
{bindFrom: ".activityDate"},
{bindFrom: ".linkedTo"},
{bindFrom: ".channelName"}
], controller: ".app.controllers.message"}
I've also tried things like this in the root of my view with no success:

bindings: [
{ from: ".app.controllers.messages.selection", to: ".app.controllers.message.data" },
{ from: ".app.controllers.messages.selection", to: ".app.controllers.message.model" }
]
Hopefully the answer is easy - I know I'm pushing up on the bleeding edge - it's really cool stuff and I just want to understand it better.

Thanks,
Eric
«1

Comments

  • I noticed a few things in my testing...

    When I manually set the model in the controller (in my first code snippet), there didn't seem to be any observers that were notified, which verified that my bindings in the second example were wrong.

    I wondered if it was related to the fact that the bindings were all for properties of a parent object (model.activityDate, model.linkedTo), and wasn't sure if a change to "model" would propagate down to any bindings on it's properties.

    So, I tried manually setting the activityDate after setting the "model" property, which also didn't work:

    var selectedItem = this.selected();
    this.app.controllers.message.set("model", selectedItem);
    this.app.controllers.message.set("linkedTo", "Some value");
    All this leads me to believe something I already know - that I don't know how the heck bindings work :)
  • That bindFrom syntax is gone in the latest version. That was all part of autobindings support which was been removed.

    Look at how the bindings are done in https://github.com/enyojs/enyo/blob/master/samples/DataRepeaterSample.js for how to bind into a DataRepeater or DataList.
  • I've also created a JSFiddle which currently demonstrates an issue with DataRepeaters within DataRepeaters ;-)

    However, it also demonstrates data binding from a selectable datarepeater to a model controller.
    http://jsfiddle.net/yTLW8/5/

    Amend the 'teamRosterhack' to have a value true to see more sensible output.

    In the example above you probably want bindingDefaults to have source:".model", rather than from:".model"
  • Thanks for the report. BTW, you have a line
    enyo.job("",enyo.bind(this,"setupData"),1000);
    
    you can now use
    this.startJob("wait", "setupData", 1000);
    
    which makes sure the job is cleaned up if you destroy the object.
  • Top tip thanks!
  • Cole's found the root cause for the nested repeaters and is filing a bug now... basically, the repeater provides a local link to model for all its children, but doesn't know where to stop.
  • ...and should be fixed in tonight's nightly build. I just merged the change.
  • ianu -

    I just now saw your reply when I logged back in. Examples are so nice! I'll go through and dissect it some more.

    After much debugging and reading source code, I finally figured out how bindings work - my misunderstanding stemmed primarily from not being clear on where the binding "lived" in the component tree. When you set up bindingDefault, I was under the impression that the default bindings would resolve from the place they were declared. Now that I understand better, I can see why that's not the case.
  • Hi Ben, the issue I demonstrated in the above fiddle still isnt fixed, unless I'm misunderstanding the bindings somehow...
  • @ianu, you're right... I've reopened the bug
  • @ericmartineau and/or @unwiredben: Can you let us know how you got bindings working? Not the ones to DataRepeater/DataList but just "normal" ones from/to a view or controller to Collection or ModelController?

    I've tried every combo using the latest nightly build and they don't seem to be firing even though I think I see 2 bindings made for each binding I declare. Also, events like this.doSomething() don't seem to be working - I hacked it by doing this.onSomething() but I know that's just plain wrong!

    Nearly hairless,
    Cage
  • Here's a chunk of code from my internal benchmark tool that uses bindings:
    /* globals formatDecimal */
    enyo.kind({
    	name: "enyoBench.FormattedTestResult",
    	classes: "formatted-test-result",
    	tag: null,
    	published: {
    		//* string to use as label for test
    		label: "",
    		//* URL to link from label
    		href: "",
    		//* title attribute for link
    		title: "",
    		//* Date.now()-based value for when the test started
    		startTime: 0,
    		//* Date.now()-based value for when the test ended
    		endTime: 0,
    		//* how long the test took in ms
    		duration: null,
    		//* FPS during the test (can be null for n/a)
    		fps: null
    	},
    	components: [
    		{
    			tag: "dt",
    			components: [
    				{kind: "enyo.Anchor", name: "testName"}
    			]
    		},
    		{tag: "dd", name: "results"}
    	],
    	bindings: [
    		{from: ".label", to: ".$.testName.content"},
    		{from: ".title", to: ".$.testName.title"},
    		{from: ".href", to: ".$.testName.href"},
    		{from: ".results", to: ".$.results.content"}
    	],
    	computed: {
    		"results": ["startTime", "endTime", "duration", "fps"]
    	},
    	results: function() {
    		var results = "";
    		if (this.startTime != null && this.endTime != null && this.duration != null) {
    			results =
    				"from " + formatDecimal(window.performance.timing.navigationStart + this.startTime, 3) + " ms " +
    				"to " + formatDecimal(window.performance.timing.navigationStart + this.endTime, 3) + " ms " +
    				"(" + formatDecimal(this.duration) + " ms)";
    		}
    		if (this.fps != null) {
    			if (results !== "") {
    				results += ", ";
    			}
    			results += formatDecimal(this.fps, 2) + " frames per second";
    		}
    		// add a non-breaking space at end to avoid empty content
    		results += "\xA0";
    		return results;
    	},
    	gotoHref: function(inSender, inEvent) {
    		if (this.href) {
    			window.location = this.href;
    		}
    		return true;
    	}
    });
  • edited September 2013
    Hi @unwiredben, thanks for code. I guess I meant from/to model/collection to view. For example, my view has this code, where UserInfo is a ModelController with a model of UserInfoModel, and activeUser is set to an instance of the model.
        controller: ".app.controllers.UserInfo",
        bindings: [
            // Of the many ways to work with `enyo.Bindings`, going through the
            // _bindings_ array is perhaps the most convenient. Note that we
            // only need to specify two properties when declaring a binding.
            //
            // Bindings are one-way by default. We will see an example of a
            // two-way binding in another component.
            {from: ".controller.activeUser", to: ".activeUser"}
        ],
        activeUser:null,
    This works if I use version 2.3.0 from about a month ago. However, when I upgrade to latest nightly, this code no longer works and bindings from my ModelControllers and Collections to Views no longer work. Plus, several events sent into Collections (using the onEventName/doEventName syntax) only execute if run a "this.onEventName()".

    I'm not sure if it's my code or something is problem in latest nightly.


    Thanks,
    Cage
  • edited September 2013
    Ah... I think the issue is the linkage... try having your create method call this.set("controller", this.app.controllers.UserInfo) instead of using a string reference. We were using a "findAndInstance" method that was more flexible, but also slow and hard to explain.
  • Okay, will try. Also, is the event handling working in latest build? the onEventName/doEventName?
  • I got the onEventName/doEventName to work (stupidity on my part) but I cannot get it to work from view -> modelcontroller or collection, even if I construct the modelcontroller/collection with an explicit owner.

    The end point is that I want the modelcontroller/collection to tell the containing view that the data has been updated so it can do something. I have tried "onModelsChanged", "onChange", etc on the containing view without success. Hence, my attempts to send in a specific event into the ModelController/Collection - which was working with a previous build but does not with the latest nightly.

    I looked at FlyApp - I really don't want to capture onChange events at the top level since I have a very complicated app and would be difficult for me to direct the change to the correct location (not to mention performance and code maintenance nightmares!).

    If you would like code samples, I can put something together but it seems as many people are having the same issue and / or not understanding how to link up the modelcontroller/collection with a specific view.

    Many thanks,
    Cage
  • events no longer get routed into controllers, other than a ViewController or Application that owns a view.
  • So what do you recommend I do to capture these events, or at least get some notification of when collections are changing?

    Specifically, I created a ArticleTagsView. It has an associated ArticleTagsCollection. My ArticleView contains the ArticleTagsView. When the ArticleModel changes, it sets the ArticleTagsCollection of ArticleTagsView, triggering a rerender with correct tags being displayed.

    FYI, I tried replacing my View with ViewController and moving my controller logic around. However, the render just removed everything from view and resulted in a blank screen (as expected, I guess, since it renders into document.body). Doesn't seem to make sense that I would have to find the DOM ID first and then render so I must be thinking about all of this incorrectly!
  • edited September 2013
    We're about to merge in the second part of the big model/collection rewrite, so I'd say ask again tomorrow... this is about to change again and we should have some extensive notes about it sent out to enyo-dev (and posted here) once the merge is done.

    One big change is that collections and models don't send events anymore but instead have a direct listener API -- enyo.DataRepeater, for example, calls an addListener method on the collection for "add" and "remove" events. This is done because it's faster for to notify a list of observers rather than bubbling out an event to an unknown set of receivers.
  • Thanks, Ben. Looking forward to it.
  • You can preview this at https://github.com/enyojs/enyo/pull/425 -- there's a few open issues that we're in the middle of resolving before the pull.
  • It appears you've gotten rid of toOne/toMany relations? :( Or should I wait until tomorrow (for some good news!)?
  • Eagerly awaiting some organized notes on the MVC release. :)
  • edited September 2013
    We have a quick code change for those relations...

    OLD:
    enyo.kind({
    	name: "enyoBench.ResultsModel",
    	kind: "enyo.Model",
    	attributes: {
    
    		// this is the collection of startup times to display
    		timestamps: {
    			relation: enyo.toMany({
    				model: "enyoBench.TimestampModel"
    			})
    		},
    
    		// this is the collection of test values
    		testResults: {
    			relation: enyo.toMany({
    				model: "enyoBench.TestResultModel"
    			})
    		}
    	}
    });
    
    NEW:
    enyo.kind({
    	name: "enyoBench.ResultsModel",
    	kind: "enyo.Model",
    	parse: function (data) {
    		data.timestamps = new enyo.Collection(data.timestamps, {model: enyoBench.TimestampModel});
    		data.testResults = new enyo.Collection(data.testResults, {model: enyoBench.TestResultModel});
    		return data;
    	}
    });
    
    If you want good news, our benchmarks show that building a collection from an array of data is about 10x faster.
  • edited September 2013
    Took a look - looking good so far.

    Apparently, params sent in options block (e.g., {q:"testQuery"} to get &q=testQuery appended to end of URL) are not making it through.

    I modified line 58 of xhr.js to get it to work (added the opts.params to the go statement) - not sure if I'm missing something or if it's a bug...
    		/**
    			Executes the _requestKind_ with the given options.
    		*/
    		go: function (opts) {
    			var Kind = this.requestKind,
    				o    = enyo.only(this._ajaxOptions, opts),
    				xhr  = new Kind(o);
    				
    			xhr.response(opts.success);
    			xhr.error(opts.fail);
    			xhr.go(opts.params);
    		},
  • edited September 2013
    And on line 190 in Store.js, should it be dd[k] = props[k]? (originally, it said dd[k] = props.
    		/**
    			Requires a hash with _key_ _value_ pairs that are the source's name by
    			which it can be referred to by this _store_ and the constructor, instance
    			or path to either in a string.
    		*/
    		addSources: function (props) {
    			var dd = this.sources;
    			for (var k in props) { dd[k] = props[k]; }
    			this._initSources();
    		},
  • edited September 2013
    @psarin, good catch on the missing `k`, I have a commit adding that. I like the idea of passing the `params` hash through to the xhr related sources, I grappled with that originally because I didn't want the feature tied so closely to model/collection but since it is a feature of the source it seems fine. So there is a commit adding support for passing a `params` hash into the `go` call of an xhr related source. Originally the intention was to allow that type of freedom to the end developer as they saw fit, e.g. overload the source or use the getUrl method to do whatever you need. This works as a feature of the source and I like that.

    Also note there should be some more information being released tomorrow regarding these changes in the developer mailing list *crossesfingers*, hope it gets through the editors.
  • @unwiredben @psarin the declaration for controller you had was fine ".app.controllers.userInfo", it still resolves paths.
  • edited September 2013
    @clinuz, thanks for info. Two more things (I think) I've noticed:

    1. if you do what Ben suggests and replace the toOne/toMany with parse version, it doesn't seem to call the parse when instantiating a Model. I see the Model constructor can accept a parse function in constructor but seems a bit kludgey (i.e., I'd have to define it twice or instantiate the Model to pass it in)

    As an example in my code:
    	enyo.kind({
    		name:"Search.ArticleModel",
    		kind:"Model",
    
    		parse: function(data){
    			data.articleDetails = new Search.ArticleDetailsModel(data.articleDetails);
    			data.topics = new Search.ArticleTopicCollection(data.articleTopics);
    
    			return data;
    		}, //attributes and rest of method deleted
    
    
    	enyo.kind({
    		name:"Search.ArticleDetailsModel",
    		kind:"enyo.Model",
    
                 // this not being called when Search.ArticleModel created
    		parse: function(data){
    			data.solrid = data.id;
    			data.title = data.docTitle;
    			data.abstractText = data.docAbstractText;
    			data.text = data.docText;
    			
    			return data;
    		},
    
    2. with the old way of toMany/toOne, it was nice to have the localKey/remoteKey. With the parse method, the "remote" key:value shows up in the "local" data structure. Just seems messy.

    Overall, liking it! Thanks
Sign In or Register to comment.