Durandal 2.1 - accessing view from viewmodel? - durandal

I have the following in my viewmodel:
dialog.getContext().reposition(vm);
But it's not working, because I'm supposed to be passing the view, not the viewmodel. How can I - from within a viewmodel - get access to its corresponding view?

To Access the view from your viewModel You have detached,compositionComplete, attached, and compositionComplete handlers to choose from.
For example from attached:
function attached(view, parent) {
console.log("This is the current view",view);//this will log the view HTML
$(view).find(".smallLoader").fadeOut();// Now fadeOut .smallLoader from this view
}
var viewModel = {
activate: activate,
title: 'Home',
attached: attached
};
return viewModel;

Related

Razor Pages - Is it possible to route to a View Component for AJAX refresh

Context:
I have a section of a view that I want to update on a regular interval via JS.
What I have done so far:
Using the information given in: Viewcomponent alternative for ajax refresh
created a view component that encapsulates the region that I want to refresh
attempted to create a custom route to a view component as follows
options.Conventions.AddPageRoute("/Components/ViewComponent/default", "FriendlyRouteAlias");
use the following script to attempt to load the (updated) view component and inject the value into a div:
<script>
var container = $(".DataToUpdate");
var refreshComponent = function () {
$.get("Route/to/view/component", function (data) { container.html(data); });
};
$(function () { window.setInterval(refreshComponent, 1000); });
</script>
Is it even possible to load a View Component this way or should I be looking at another way of accomplishing this?
As the commenters suggested, I was able to get it working by using an MVC controller action to return the view component directly. I will add that I used an MVC controller rather than an API controller because I was dealing with a view rather than data. (see Difference between ApiController and Controller in ASP.NET MVC)

Call a custom attribute method from the view model

I have a custom attribute with a method to show and hide some HTML content, I've attached the attribute to an element in a view model.
How can I call a method defined in the custom attribute from the view model?
To access the custom attribute's view-model, just put the custom attribute on the element a second time, but this time put .ref="viewModelPropertyName" on to the attribute. Then, in the parent view-model, you can access methods on the attribute using viewModelPropertyName (or whatever name you gave it). You can see an example of this here: https://gist.run/?id=9819e9bf73f6bb43b07af355c5e166ad
app.html
<template>
<require from="./has-method"></require>
<div has-method="hello" has-method.ref="theAttribute"></div>
<button click.trigger="callMethod()">Call method</button>
</template>
app.js
export class App {
callMethod() {
const result = this.theAttribute.someMethod('blah');
}
}
has-method.js
export class HasMethodCustomAttribute {
someMethod(foo) {
console.log('someMethod called with foo = ' + foo + ', this.value = ' + this.value);
return `Hello ${foo}`;
}
}
There are some ways to do it, but I believe the ideal would be binding a property from your custom-attribute to your view-model. For example:
MyCustomAttribute {
#bindable showOrHide; //use this to show or hide your element
}
MyViewModel {
visible = false;
}
Usage:
<div my-custom-attribute="showOrHide.bind: visible"></div>
So, whenever you change visible you will also change showOrHide.
Nevertheless, is good to remember that Aurelia already has a show and if custom-attributes:
<div show.bind="visible" my-custom-attribute></div>
<div if.bind="visible" my-custom-attribute></div>
Make sure if you really need to create this behaviour in your custom-attribute.
This can be done without the need for a ref. Here is an example that shows how.
It calls a showNotification method on the custom attribute from the custom element using the custom attribute.
In the custom attribute:
#bindable({ defaultBindingMode: bindingMode.twoWay }) showNotificationCallback: ()=> void;
bind() {
this.showNotificationCallback = this.showNotification.bind(this);
}
showNotification() {
// Your code here
}
In the custom element view (Note the absence of parens in the value of this binding):
<div notification="show-notification-callback.bind: showSaveSuccessNotification;></div>
In the custom element view-model:
// Show the save success view to the user.
if (typeof this.showSaveSuccessNotification=== 'function') {
this.showSaveSuccessNotification();
}

How to tear down an enhanced fragment

I am working on an existing SPA where we replace components with Aurelia components step by step. We use the enhance API of the TemplatingEngine. That works pretty well but we also need to tear down those enhanced fragments (remove event listeners, ...) when moving to another part of the application (no page reload).
My idea is to keep the aurelia instance in the page and reuse it.
Currently I enhance fragments like this:
function enhanceFragment(targetElement) {
function proceed() {
let aurelia = window.DFAurelia;
let engine = aurelia.container.get(TemplatingEngine);
engine.enhance({
container: aurelia.container,
element: targetElement,
resources: aurelia.resources
});
}
if (!window.DFAurelia) {
bootstrap(async aurelia => {
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.eventAggregator()
.developmentLogging()
.globalResources('app/df-element');
await aurelia.start();
window.DFAurelia = aurelia;
proceed();
});
} else {
proceed();
}
}
The HTML I enhance looks like:
<df-element></df-element>
I tried this in a function of the custom element itself (DfElement::removeMyself()):
let vs: ViewSlot = this.container.get(ViewSlot);
let view: View = this.container.get(View);
vs.remove(view);
vs.detached();
vs.unbind();
but I get an error when getting the view from the container (Cannot read property 'resources' of undefined). I called this function from a click handler.
Main question: how to manually trigger the unbind and detached hooks of the DfElement and its children?
Bonus questions: properties of my aurelia instance (window.DFAurelia) root and host are undefined: is that a bad thing? Do you see any potential issue with this way of enhancing (and un-enhancing) fragments in the page?
Use the View returned from the enhance() method.
The enhance() method returns the enhanced View object. It is a good practice to manage the teardown from the same location where you call enhance(), as you may not be able to trust an element to remember to tear itself down. However, you can always register the View instance with the enhance container to access it from within the custom element.
function proceed() {
let aurelia = window.DFAurelia;
let container = aurelia.container;
let engine = container.get(TemplatingEngine);
let view = engine.enhance({
container: container,
element: targetElement,
resources: aurelia.resources
});
container.registerInstance(View, view);
}
This will tell the DI container to respond to calls for View with this View.
import { inject, Aurelia, View } from 'aurelia-framework';
#inject(Aurelia, Element)
export class DFCustomElement {
// element is passed to the constructor
constructor(aurelia, element) {
this.container = aurelia.container;
this.element = element;
}
// but View is only available after attached
attached() {
this.view = this.container.get(View);
}
removeMyself() {
this.element.remove();
this.view.detached();
this.view.unbind();
}
}
Using the created(view) lifecycle method
A much better practice would be to use the created(view) lifecycle method in your custom element.
import { inject } from 'aurelia-framework';
#inject(Element)
export class DFCustomElement {
constructor(element) {
this.element = element;
}
created(view) {
this.view = view;
}
removeMyself() {
this.element.remove();
this.view.detached();
this.view.unbind();
}
}
This is a much more straightforward, best practices way of having a custom element grab its own View. However, when trying to write this answer for you, I tested nesting a custom element in a <compose> element. The result was that the View in my custom element was actually the View for my <compose> element, and removeMyself() removed the <compose> entirely.

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.

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.