I've just started using spine and I'm having trouble understanding a few things...
class Spk.Register extends Spine.Controller
render: ->
#html("<h1>Registration!</h1>")
...
Zepto(function($) {
new Spk.Register({
el: $("#data")
});
});
...I was expecting this to replace any html in the #data element with the html passed to #html when the controller is instantiated, but it doesn't, nothing happens at all.
I've also tried putting the render method in the constructor function but again nothing happens.
How would I replace the html in body with given html when the controller is instantiated?
The problem is the render() method isn't called.
You have to call it explicitly after controller instantiated.
Anyway, I think you shouldn't do any rendering in the constructor.
Better option is:
to have a model (MVC architecture) which triggers particular event
after the data loaded from the server,
the controller should define event handler for that event and it will render the view.
EDIT
Just very simple code snippet how it could be (CoffeeScript, using jQuery):
The Task model class:
class Task extends Spine.Model
#configure 'Task', 'name', 'deadline'
#fetch () ->
Task.deleteAll()
# ... load data from the server ...
Task.create name: 'foo', deadline: '2012-11-22' # create local instance(s)
Task.trigger 'data-loaded'
return
The controller:
class Tasks extends Spine.Controller
constructor: ->
super
init: () ->
#routes
'list': (params) ->
Task.fetch()
return
Task.bind 'data-loaded', () =>
#render()
return
return
render: () ->
#el.render Task.all()
return
The initialization code (another possibility could be Spine.js controller stack):
tasksCtrl = new Tasks el: $('.task-list')
tasksCtrl.navigate 'list'
Note that it requires also route.js (included in Spine.js) and I've used Transparency template engine (it's #el.render() meth). Then the template looks like:
<div class="task-list">
<div class="task">
<span data-bind="name"></span>
<span data-bind="deadline"></span>
</div>
</div>
Related
I have a vue single file component, which have a custom class instance as property: now i want to bind a event to a method of this class instance but i'm having issues, there is a simplified version of my code files
VueJS single file component
<template>
<div #click="class_prop.method"></div>
</template>
export default {
data() {
return {
class_prop: new CustomClass(),
};
},
}
CustomClass
class CustomClass {
constructor(){
this.prop = 'default_value';
}
method(){
this.prop = 'new_value';
console.log(this.prop);
}
}
The error
When clicking on the page element i receive this error:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'prop' of null"
But when i try to call the custom class method from the browser console (i'm using Chrome with Vue Devtools extension) i get NO error, it works correctly:
$vm0.class_prop.method()
Since i get two different behaviours of my code i cannot tell if it is my class wrong, the vue single file component, or something else.
Behind the scenes Vue somehow called or applied your method using null. Thus the error you've mentioned:
Error in v-on handler: "TypeError: Cannot read property 'prop' of null"
What can you do to workaround the problem?
You can use a lambda expression, so you can have full control of the object owner of the method, like this:
<div #click="() => class_prop.method()"></div>
You can also use the class instance in a method like this:
export default {
data: () => ({
foo: new CustomClass(),
}),
methods: {
bar() {
this.foo.method();
},
},
}
And then in your template:
<div #click="bar">{{ foo.prop }}</div>
What you see is not Vue's fault, it's just plain JavaScript.
Here is a citation from great JS learning resource
The consequences of unbound this
If you come from another programming language, then you are probably used to the idea of a "bound this", where methods defined in an object always have this referencing that object.
In JavaScript this is “free”, its value is evaluated at call-time and does not depend on where the method was declared, but rather on what object is “before the dot”.
Here is very simple example of consequences of above paragraphs (and why your code does not work):
class CustomClass {
constructor() {
this.prop = 'default_value';
}
method() {
this.prop = 'new_value';
console.log(this.prop);
}
}
let instance = new CustomClass()
instance.method() // this works OK
let f = instance.method
f() // this does not! f is "unbound" ....have no "this"
Good Day Fellows,
Quick summary: how can I use custom option merge strategies on an individual basis per component and not globaly?
My problem:
I am extending my components via Mixins and it is working great so far. However, while it is working great with the likes of component methods, I often need to override some lifecycle hooks, like mounted, created, etc. The catch is, Vue - by default - queues them up in an array and calls them after another. This is of course defined by Vues default merge strategies.
However in some specific cases I do need to override the hook and not have it stack. I know I can customize Vue.config.optionMergeStrategies to my liking, but I want the mergeStrategy customized on a per component basis and not applying it globably.
My naive approach on paper was to create a higher function which stores the original hooks, applies my custom strategy, calls my component body and after that restores Vues original hooks.
Let's say like this
export default function executeWithCustomMerge(fn) {
const orig = deep copy Vue.config.optionMergeStrategies;
Vue.config.optionMergeStrategies.mounted = (parent, child) => [child];
fn();
Vue.config.optionMergeStrategies = deep copy orig;
}
And here's it in action
executeWithCustomMerge(() => {
Vue.component('new-comp', {
mixins: [Vue.component("old-comp")],
},
mounted() {
//i want to override my parent thus I am using a custom merge strategy
});
});
Now, this is not going to work out because restoring the original hook strategies still apply on a global and will be reseted before most hooks on my component are being called.
I wonder what do I need to do to scope my merge strategy to a component.
I had a look at optionMergeStrategies in more detail and found this interesting quote from the docs (emphasis mine):
The merge strategy receives the value of that option defined on the parent and child instances as the first and second arguments, respectively. The context Vue instance is passed as the third argument.
So I thought it would be straightforward to implement a custom merging strategy that inspects the Vue instance and looks at its properties to decide which strategy to use. Something like this:
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal, vm) {
if (vm.overrideCreated) {
// If the "overrideCreated" prop is set on the component, discard the mixin's created()
return [vm.created];
}
return mergeCreatedStrategy(toVal, fromVal, vm);
};
It turns out though that the 3rd argument (vm) is not set when the strategy function is called for components. It's a new bug! See https://github.com/vuejs/vue/issues/9623
So I found another way to inform the merge strategy on what it should do. Since JavaScript functions are first-class objects, they can have properties and methods just like any other object. Therefore, we can set a component's function to override its parents by setting a property on it and looking for its value in the merge strategy like so:
Vue.mixin({
created() {
this.messages.push('global mixin hook called');
}
});
const mixin = {
created() {
this.messages.push('mixin hook called');
},
};
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal) {
if (fromVal.overrideOthers) {
// Selectively override hooks injected from mixins
return [fromVal];
}
return mergeCreatedStrategy(toVal, fromVal);
};
const app = {
el: '#app',
mixins: [mixin],
data: { messages: [] },
created() {
this.messages.push('component hook called');
},
};
// Comment or change this line to control whether the mixin created hook is applied
app.created.overrideOthers = true;
new Vue(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>Messages from hooks</h1>
<p v-for="message in messages">{{ message }}</p>
</div>
i trying to build logic to translate only part of the page(module) with i18n lib.
i have set i18n globally to change language on page when i change language, but i would like to have one module on that page (like some kind of preview for email) on different language which i can change on that module via some dropdown field. Like some kind of scoped i18n.
I'm using aurelia-i18n 1.4.0 version.
is it possible to set <span t="messages.hello_message">hello<span> to watch for local module changes for language and not the global one, but again to use the same translation files as global does.
Did anyone have some similar problem or idea how can I do this? thanks. :)
You can't do that out of the box. When you change the active locale using setLocale the method fires an event and signals an update to binding behaviors https://github.com/aurelia/i18n/blob/master/src/i18n.js#L54.
The TCustomAttribute listens for those changes and automatically rerenders bindings. What you could do though is create your own custom attribute as seen here https://github.com/aurelia/i18n/blob/master/src/t.js and override the bind and unbind methods where you define the condition when the update of translations should happen.
--- Update with example ---
Ok so here's a small example what I was thinking about, might not be the nicest way but it should do it.
In your main.js add a new globalResources
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.plugin('aurelia-i18n', (instance) => {
...
})
.globalResources("./foo-custom-attribute") // <-- this here
now create a file foo-custom-attribute.js
import {TCustomAttribute} from 'aurelia-i18n';
import {customAttribute} from 'aurelia-framework';
#customAttribute('foo')
export class FooCustomAttribute extends TCustomAttribute {
constructor(element, i18n, ea, tparams) {
super(element, i18n, ea, tparams);
}
bind() {
this.params = this.lazyParams();
if (this.params) {
this.params.valueChanged = (newParams, oldParams) => {
this.paramsChanged(this.value, newParams, oldParams);
};
}
let p = this.params !== null ? this.params.value : undefined;
this.service.updateValue(this.element, this.value, p);
}
unbind() {}
}
This essentially creates a new attribute called foo, which extends the TCustomAttribute and overrides the bind/unbind methods to exclude the signaling and listening to language changed events.
In your view you can now use
<span t="demo"></span>
<span foo="demo"></span>
Toggling the language now will change the t attribute as usual but will keep the foo as it is.
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();
}
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.