Is it possible to call a custom-bound widget's methods in Durandal? - durandal

I have created and registered a widget in Durandal, so now I am able to use it in other views using this markup:
<div data-bind="MyWidget: { activationData }" />
I would like to call methods on that widget from the parent view model, for example:
ParentViewModel.prototype.buttonClick = function() {
this.myWidget.doSomething();
}
Is there a neat way to access a widget composed in this way from the parent view model?

I've been working on this problem since posting the question, and the best solution I have come up with is this:
Add an observable, let's call it "myWidget", to the parent view model
Pass the empty observable to the widget upon activation, using widget binding
During activation, the widget sets the parent's observable to itself
For example, in the View Model definition:
this.myWidget = ko.observable(null);
Use widget binding in the parent view:
<DIV data-bind="MyWidget: { theirWidget : myWidget }" />
Set the parent view's reference in the widget's activate method:
MyWidget.prototype.activate = function(activationObject) {
activationObject.theirWidget(this);
}
While this is a reasonable solution, I'll wait and see whether anyone else provides an alternative before accepting this answer.

Related

Best way of executing js function located in parent page from custom element

Aurelia: I have a custom element that should execute a function that located in the parent page. the custom element doesn't "know" what function it should execute - it depends on the parent page, and currently I send the name of the function as an attribute to the custom element (the attribute - on-focus-out-action-name):
<form-input field-name="firstName" title="First Name" on-focus-out-action-name ="validateInput()" />
I manage to run the function when it has no params, but when I want to send params (simple string type which is also sent as attribute) - no success
Is there a better way to do it ?
The best way was if I could pass the function as an object (Dependency Injection ?)
You should use the call binding command for this. The way to pass parameters to a function when using the call bind is a bit wonky, but once you understand it, it's easy.
In the custom element, you will pass an object to the bound function. Each property of this function will be matched to the named parameters in the binding. Let's look at it in action. In the page VM, I'll have a function:
pageFunction(paramOne, paramtwo) {
//.. stuff happens
}
This function will be called by a custom element. So in the page view, we will write the binding like this:
<my-element some-func.call="pageFunction(paramOne, paramTwo)"></my-element>
Inside my-element's VM, we can call the bound function, and pass the parameters to it like this:
this.someFunc({paramOne: this.someProp, paramTwo: this.otherProp});
I've created a runnable gist example here: https://gist.run/?id=864edc684eb107cdd71c58785b14d2f9
I prefer using a CustomEvent for this, this makes it clear in the template what is going on.
In your component you dispatch the event like this - the "details" can be any object/data you want:
let event = new CustomEvent('on-focus-out-action-name', {
detail: <some-data-you-want-to-send>,
bubbles: true
});
this.element.dispatchEvent(event);
You'll also need to inject the element in the constructor (you'll also need to use autoinject or inject Element manually)
constructor(private element: Element) {}
Your parent template would then look something like this:
<form-input field-name="firstName"
title="First Name"
on-focus-out-action-name.delegate="validateInput($event)" />
And in your parent component, you use the data you sent like this:
validateInput(event) {
let data = event.detail;
// do stuff
}
Check out this blog post for more details https://ilikekillnerds.com/2015/08/aurelia-custom-elements-custom-callback-events-tutorial/
I managed to do it in Aurelia: This is the custom element with the foucusout.trigger which calls to the focusoutAction in the appropriate timing:
<template>
<label>
${title} - ${fieldName}<input title.bind="title"
focusout.trigger="focusoutAction()" focusin.trigger="focusInAction()"
class="${cssClass}" />
</label>
</template>
This is the usage of the custom element in the parent view with the focusout.call attribute:
<form-input field-name="firstName" title="First Name"
place-holder="Enter first name"
on-focusout.call="validateInput(nameOfElement)" />
And this is the relevant code in the view model of the custom control:
#bindable onFocusout;
focusoutAction() {
var args =
{
nameOfElement: this.fieldName
};
this.onFocusout(args);
}

Aurelia <compose> model.bind what triggers a change event?

In Aurelia we have the ability to dynamically compose viewmodels and views using the <compose> element. We can also supply an object of data via model.bind which is then accessible via the activate method of the provided viewmodel.
My question is, what conditions trigger a change event on the provided model data? If I change a property on the object I provide, will my activate method which gets this object as the first parameter see the change? Or does the entire object need to be replaced to trigger a change?
The activate(model) gets called once when the model is bound to the view model. When the model attributes change, those changes will be reflected in the composed view model because the model is a reference, not a copy.
For example, say I have a view / view model that is a target for a route as follows (this example is not a perfectly clean example because I was experimenting with other things as well, but it should be clear enough):
View: This view creates two sections separated by an <hr>. The top just displays the model.message for each view. The bottom creates a <compose> for each view.
<template>
<div repeat.for="view of openViews">
<p>${view.model.message}</p>
</div>
<hr>
<div repeat.for="view of openViews">
<compose view-model.bind="$parent.getViewFromType(view)" model.bind="view.model">
</compose>
</div>
</template>
View Model: note that the openViews is at a global scope. This is so that if we navigate away from the route and then return to the route, any changes made to the view.model will be retained. If the model was on the ZenStudio object, the object gets destroyed and recreated when the route moves away and returns to this view and therefore would lose the data.
var openViews = [
{ viewType: "", model: { message: "View 1"}},
{ viewType: "", model: { message: "View 2"}},
{ viewType: "", model: { message: "View 3"}}
];
export class ZenStudio {
constructor() {
}
created() {
}
get openViews() {
return openViews;
}
getViewFromType(view) {
// TOOD Load plugins and use the views defined by plugins
return "./views/editor-view";
}
}
The editor-view view and view-model are as follows:
<template>
<h1>${model.message}</h1>
<form>
<input type="text" value.bind="model.message">
</form>
</template>
View-model:
export class EditorView {
constructor() {
}
created(owningView, thisView) {
this.view = thisView;
this.parentView = owningView;
}
activate(model) {
// Keep track of this model
this.model = model;
}
}
You'll see that the ZenStudio view is displaying the same model.message as the EditorView. When the user edits the value of the message inside the <input>, the values correctly change in the top level view, as well as within the corresponding <compose> view.
While I don't have an example, if you added another item to openViews list then that would add another sub view and another line in the top level view displaying the new message. The repeat.for listens to the additions and subtractions made to the list and correctly creates / removes the composed elements.
Hopefully that answers your question.

Durandal view composition order

Durandal 2.0 - view composition order
I have view, that has a number of child views that I am using just to break up the HTML into separate files.
index.html(index.js)
- menu.html
- header.html
- footer.html
The menu view is populated by the index view after it's loaded.
Inside the index.html I compose the menu view like this (there is no module menu.js, just html composition here):
<!--ko compose: { view: 'menu'}--><!--/ko-->
The problem is the order of the view composition.
I have an attached event wired up in index.js that then calls a function to populate the menu div that's sat in menu.html.
But, the attached view event is called before the menu.html view has been composed.
The events looks something like this from the console:
Binding views/footer
Binding views/index
binding complete: <section data-view=​"views/​index" style=​"display:​ none;​">​…​</section>​
[Index] Main View Attached
Binding views/header
Binding views/menu
Binding views/menudropdown
So, the main view is attached before the children.
Is there a way to change the composition order, or wait for all the child views to be composed/loaded before the main view is attached/complete?
Thanks
The short answer is "no" - you can't change the composition order.
The longer answer: perhaps I've misunderstood, but it sounds a little bit suspicious that you're populating the menu div from your view model. Could you use a custom binding to do this instead? If you can do it in a binding, then you could have a look at using a Delayed Binding Handler. From the documentation:
Sometimes your binding handler needs to work with an element only
after it is attached to the DOM and when the entire composition of the
view is complete. An example of this is any code that needs to measure
the size of an HTML element. Durandal provides a way to register a
knockout binding handler so that it does not execute until the
composition is complete. To do this, use
composition.addBindingHandler.
Alternatively, if you're happy with whatever code is running in your viewModel, you probably want to use the compositionComplete event rather than the attached event. From memory, the attached events run from parent to child (which is why the event is being called on index before the menu has been composed). In contrast, compositionComplete bubbles from child to parent:
Finally, when the entire composition process is complete, including
any parent and child compositions, the composition engine will call
the compositionComplete(view, parent) callback, bubbling from child to
parent.
You can attach a compositionComplete handler on your viewModel in the same fashion as an activate method:
// Secured Shell
define([], function () {
var viewModel = {
activate: function () {
// do stuff
},
compositionComplete: function (parent, child, settings) {
// do more stuff
debugger;
}
};
return viewModel;
});
Hope that helps.

Stop Durandal from looking for the viewmodel

Hi I have a situation where I need to compose only a view and not a viewModel for this I have set this composition statement in my html:
<!-- ko compose: { view : content }-->
<!--/ko-->
Content represent an observable from my viewmodel.
The problem is that it seems the framework is also trying to download the viewmodel which does not exist and has no reason to exist.
Does anyon no how to stop Durandal from looking for the viewModel?
I have tryed setting the model : null but it did not work
You can't stop Durandal looking for the view model if you're using the compose binding, but there are a number of things you can do to prevent loading a new model:
Point Durandal to a dummy object to use as the model (e.g. create a singleton dummyModel.js);
Use a "dumb" object (for example an array) for your model:
<!-- ko compose: { view : content, model: [] }--><!--/ko-->
Use the current model, and turn off activation (to prevent activate being called on the model twice):
<!-- ko compose: { view : content, model: $data, activate: false }--><!--/ko-->
Basically, Durandal doesn't care what you give it as a model, as long as it has something to use. Note that it will still bind whatever model you specify to your view though!
try this
<div>
<div data-bind="compose:'views/content.html'"></div>
</div>
I am not sure if this will answer your question but I did come across a similar situation where I wanted to load views of my application which had no viewmodels. I created a module which given the view would load the view for me. All I had to was overwrite the getView function of my custom viewmodel which loaded my views.
//viewLoader --> it's job is to load the views which do not have any viewmodels
define(['plugins/router], function(router){
return {
getView: function() {
return "views/"+router.activeInstruction().config.file +".html";
}
}
}

Loading jquery plugin result into Durandal view

I am using the Durandal Starter Template for mvc4. I have set the following simple View:
<section>
<h2 data-bind="html: displayName"></h2>
<h3 data-bind="html: posts"></h3>
<button data-bind="click: getrss">Get Posts</button>
<div id="rsstestid" ></div>
</section>
and ViewModel:
define(function (require) {
var http = require('durandal/http'),
app = require('durandal/app');
return {
displayName: 'This is my RssTest',
posts: ko.observable(),
activate: function () {
return;
},
getrss: function () {
$('#rsstestid').rssfeed('http://feeds.reuters.com/reuters/oddlyEnoughNews');
return;
}
};
});
As you can see, it is simply using the zRssReader plugin to load posts into a div when the 'Get Posts' button is clicked. Everything works fine, the display name is populated and the posts show up as expected.
Where I am having trouble is when I try to eliminate the button and try to load the posts at creation time. If I place the plugin call in the activate function, I get no results. I assume this is because the view is not fully loaded, so the element doesn't exist. I have two questions:
How do I delay the execution of the plugin call until the view is fully composed?
Even better, how do I load the plugin result into an the posts observable rather than using the query selector? I have tried many combinations but no luck
Thanks for your help.
EDIT** the below answer is for durandal 1.2. In durandal 2.0 viewAttached has changed to attached
Copy pasted directly from durandaljs.com
"Whenever Durandal composes, it also checks your model for a function called viewAttached. If it is present, it will call the function and pass the bound view as a parameter. This allows a controller or presenter to have direct access to the dom sub-tree to which it is bound at a point in time after it is injected into its parent.
Note: If you have set cacheViews:true then viewAttached will only be called the first time the view is shown, on the initial bind, since technically the view is only attached once. If you wish to override this behavior, then set alwaysAttachView:true on your composition binding."
--quoted from the site
There are many ways you can do it but here is just 1 quick and dirty way:
<section>
<h2 data-bind="html: displayName"></h2>
<h3 data-bind="html: posts"></h3>
<button data-bind="click: getRss">Get Posts</button>
<div id="rsstestid"></div>
</section>
and the code:
define(function (require) {
var http = require('durandal/http'),
app = require('durandal/app');
var $rsstest;
return {
displayName: 'This is my RssTest',
posts: ko.observable(),
viewAttached: function(view) {
$rssTest = $(view).find('#rsstestid');
},
getRss: function() {
$rssTest.rssfeed('http://feeds.reuters.com/reuters/oddlyEnoughNews');
}
};
});
In general, I think it's wise to refrain from directly touching UI elements from within your view model.
A good approach is to create a custom KO binding that can render the rss feed. That way, you're guaranteed that the view is in place when the binding executes. You probably want to have the feed url exposed as a property on your view model, then the custom binding can read that when it is being updated.
Custom bindings are pretty simple - if I can do it, then it must be :)
Here's a link to the KnockOut custom bindings quickstart: http://knockoutjs.com/documentation/custom-bindings.html
I too am having the same problem, I'm trying to set a css property directly on an element after the durandal view model and view are bound together. I too assume that it's not working because the view is not fully composed at the point I am setting the value.
Best I have come up with is using the viewAttached lifecycle event in durandal, which I think is the last event in the loading cycle of a durandal viewmodel, and then using setTimeout to delay the setting of the property still further.
It's a pretty rubbish workaround but it's working for now.
var viewAttached = function (view) {
var _this = this;
var picker = new jscolor.color($(view).children('.cp')[0], {
onImmediateChange: function() {
_updateCss.call(_this, this.toString());
}
});
picker.fromString(this.color());
setTimeout(function() {
_updateCss.call(_this, _this.color());
}, 1000);
};
var activate = function (data) {
system.log('activated: ' + this.selectors + ' ' + this.color());
};
var deactivate = function (isClose) {
system.log('deactivated, close:' + isClose);
};
return {
viewAttached: viewAttached,
deactivate: deactivate,
activate: activate,
color: this.color
};
I was having a similar issue with timing. On an initial page load, where a partial view was being loaded on the page I could call the viewAttached function and use jQuery to bind some elements within the partial view. The timing worked as expected
However, if I navigated to a new page, and then back to the initial page, the same viewAttached + jQuery method failed to find the elements on the page... they had not yet been attached to the dom.
As best as I have been able to determine (so far) this is related to the transition effects in the entrance.js file. I was using the default transition which causes an object to fade out and a new object to fade in. By eliminating the fadeOutTransition (setting it to zero in entrance.js) I was able to get the viewAttached function to actually be in sync with the partial views attachment.
Best guess is that while the first object is fading out, the incoming object has not yet been attached to the dom but the viewAttached method is triggered anyway.