I am building an application with Durandal 2.0.
My shell view looks like this:
<div>
<header id="nav" data-bind="compose: 'viewmodels/nav', activate: false">
</header>
<section id="content" class="main container-fluid" data-bind="router: { transition: 'entrance' }, activate: false" style="overflow: auto">
</section>
<footer>
<!--ko compose: {view: 'footer'} --><!--/ko-->
</footer>
</div>
In the nav section, I want to have my tabs and a drop-down list of users (both of which are retrieved from a web service). Selecting a user from the dropdown will navigate to a new URL which will update the content section. (the route looks something like localhost/#user1/tab2).
Problem: I need to know the selected user from the nav section before I can retrieve the data for the content section, but the content section is activating before I have retrieved the users.
This is really only an issue for the initial page load, since the user list is only retrieved once.
Is there a way to tell the content section to wait until the nav section is done loading?
Is there a better way to go about this than what I'm doing?
The nav activate function looks like this:
function activate(context) {
return dataservice.getUsers().then(function () {
//do stuff
});
});
This activate function gets called first, and dataservice.getUsers() is called, but then the activate function of the content module gets called before the "do stuff" part happens (and before the data from the getUsers call is returned in the dataservice). Maybe there's a problem with my promises?
Edit
I've put together a dFiddle with some actual code that shows what I'm talking about: http://jerrade.github.io/dFiddle-2.0/#test/dashboard
The code is here: https://github.com/jerrade/dFiddle-2.0
nav.js
function activate(context) {
console.log('Nav View Activated');
if (vm.impersonateUsername == undefined)
vm.impersonateUsername = getUsernameFromWindowLocation();
return dataservice.getPageDetailForEmployee(vm.loggedInUsername, vm).then(function () {
console.log("Page details retrieved");
// I want to do something here before the dashboard activates.
});
}
dashboard.js
function activate(username) {
console.log('Dashboard View Activated');
//vm.username = nav.selectedImpersonateEmployee().Username;
return dataservice.getDashboard(nav.impersonateUsername, dashboard);
}
Open the page and watch the console. You'll see (among other things)
Nav View Activated
Dashboard View Activated
Page details retrieved
What I really want is for the Page details to be retrieved before the Dashboard view activates. I've actually rejiggered things so that this isn't currently a problem anymore, but it may crop again down the road.
It doesn't seem like what I'm trying to do should be this complicated. Unless I'm pounding a square peg into a round hole here?
The easiest thing to do would be to add an observable called something like isUserLoaded on your view model. Then, apply an if binding around the content section:
<div>
<header id="nav" data-bind="compose: 'viewmodels/nav', activate: false">
</header>
<!-- ko if: isUserLoaded -->
<section id="content" class="main container-fluid" data-bind="router: { transition: 'entrance' }, activate: false" style="overflow: auto">
</section>
<!-- /ko -->
<footer>
<!--ko compose: {view: 'footer'} --><!--/ko-->
</footer>
</div>
Once you've loaded the user, you can update the observable to true and your content binding should fire.
Edit
If that does't work it sounds like your router must still be resolving the content view model (which may point to a routing problem? Hard to say without seeing your entire solution).
Anyway, if its not a routing issue, then you could employ a solution where your content model returns an unresolved promise from its activate method; then resolve that promise when the user is loaded. For example, something along these lines:
userModel.js:
define([], function () {
// single instance to track the loaded user
var userModel = {
selectedUser: ko.observable()
};
return userModel;
});
navModel.js:
define(["dataService", "userModel"], function(dataService, userModel) {
// view model for the nav bar
var navModel = {
// model definition
};
// load the user
navModel.activate = function() {
return dataService.getUsers().then(function(response) {
// Push the user to the userModel
userModel.selectedUser(response.user);
});
};
return navModel;
});
contentModel.js:
define(["userModel"], function (userModel) {
var contentModel = {
// model definition
};
contentModel.activate = function () {
// Prevent activation until user is resolved
var deferred = $.Deferred();
var subscription = userModel.selectedUser.subscribe(function (user) {
if (!user)
return;
subscription.dispose();
deferred.resolve();
});
return deferred.promise();
};
return contentModel;
});
If you don't want to use an observable for the userModel.selectedUser property, you could instead use another deferred, or fire an event when the user is loaded, etc etc. Whatever floats your boat!
Related
I'm working on a project which expects a lot of places where I have to implement upload file component with different styles. I want to create highly customizable component which design I can modify easily and also I don't want to repeat myself and want to extract all the common logic into one place.
At this moment I have a vue.js version 2.2.2 and bulma css framework. I have a basic implementation of this component with only one design available. It supports a few states which represents current upload status:
component is waiting for the input
upload started
upload finished successfully
upload failed
Also this component is responsible for the upload process.
At this my component has a lot of responsibilities:
1. it knows how to deal with statuses:
<p v-if="isWaiting">
...
</p>
<div v-if="isLoading" class="is-loading">
<p class="title">{{currentPercent}} %</p>
</div>
...
data() { return {
currentStatus = Statuses.waiting
}}
computed: {
isWaiting() {
return this.currentStatus === Statuses.waiting;
},
...
}
it knows how to upload the data and count the current percent of data which is already transfered:
selectedFileChanged: async function(event) {
if (event.target.files.length === 0) return;
this.currentStatus = Statuses.uploading;
this.selectedFile = event.target.files[0];
const formData = new FormData();
formData.append("file", this.selectedFile);
try {
const result = (await axios.post("some url", formData, {
onUploadProgress: progress => {
const loaded = progress.loaded;
const total = progress.total;
this.currentPercent = Math.floor((loaded * 100) / total);
}
})).data;
this.currentStatus = Statuses.uploaded;
this.$emit("fileUploaded", {
file: this.selectedFile,
payload: result
});
} catch (exception) {
this.currentStatus = Statuses.error;
}
}
it has only one style which I can use
you can find the full code here: https://codesandbox.io/s/vue-template-gz2gk
So my question is: how to build this component to have an opportunity to change it's style easily and how to deal with upload statuses?
It seems to me that:
1. the component shouldn't know that axios is and how to upload the data;
2. the component should only be responsible for the current status and how to display it
I can introduce a new upload service which will know how to upload the data and add a new prop (current status) for upload file component and change it from the parent component. But in this case I will write the same code for all instances of the component.
Does someone know best practices of how-to create such customizable component?
UPDATE 1
I've tried to implement this functionality using slots and ended up with: https://codesandbox.io/s/vue-template-pi7e9
The component still knows how to upload the data, but now I can change the style of upload component.
So the new question is: how to work with slots and do not transfer a lot of variables and how to deal with uploading. I don't want my component to know how to upload the data :(
I've finished the component in the followig way:
In my parent component I have two different styles for my component:
<div id="app" class="container box">
<upload-file url="url"></upload-file> <-- the default one with statuses outside -->
<upload-file url="url"> <-- custom one which look like a box with statuses in it -->
<template #fileselect="{status, change}">
<custom-file-upload :status="status" #change="change"></custom-file-upload>
</template> <-- I've used custom-file-upload component here and tnjected all the properties from default implementation -->
</upload-file>
</div>
My default file input is nothing but a slot with default implementation:
<template>
<div>
<slot name="fileselect" :change="selectedFileChanged" :status="status">
<input id="fileselect" #change="selectedFileChanged" class="file-input" type="file">
<div class="help is-info" v-if="isWaiting">Waiting</div>
<div class="help is-success" v-if="isUploaded">Uploaded</div>
<div class="help is-info" v-if="isUploading">Uploading {{currentPercent}} %</div>
<div class="help is-error" v-if="isFailed">Failed</div>
</slot>
</div>
</template>
and what is the code looks like:
name: "upload-file",
props: ["url"], // url which will be used in upload request
data() {
return {
currentStatus: 1,
selectedFile: null,
currentPercent: 0
};
},
computed: {
someOtherProperties,
status() {
return {
isWaiting: this.isWaiting, // this.currentStatus === 1
isUploaded: this.isUploaded,
isUploading: this.isUploading,
isFailed: this.isFailed,
currentPercent: this.currentPercent,
selectedFile: this.selectedFile
};
}
},
methods: {
selectedFileChanged: function(event) {
this.selectedFile = event.target.files[0];
const xhr = new XMLHttpRequest();
// some handlers for XHR
xhr.open("POST", this.url, true);
xhr.send(formData);
}
}
Now I can use the file upload component with different styling but it will encapsulate in a base implementation all the status handling and upload logic.
I hope this solution will help someone :)
To view the full version of the code, please follow https://codesandbox.io/s/vue-template-pi7e9
I need to submit a form programmatically, but I need it to preventDefault as well.
Right now I have the following:
submit() {
this.$refs.form.submit()
}
It is working fine, but I cannot prevent default on the submit which in the end, refreshes the page.
Short answer
You can add the .prevent modifier to the #submit (or any other v-on you're using), like this:
<form #submit.prevent="myMethod">
<button type="submit"></button>
</form>
In the case of submitting a form, this will prevent the default behavior of refreshing the page.
Long answer
There are several ways to modify events.
From the Vue 3 docs:
It is a very common need to call event.preventDefault() or
event.stopPropagation() inside event handlers. Although we can do this
easily inside methods, it would be better if the methods can be purely
about data logic rather than having to deal with DOM event details.
To address this problem, Vue provides event modifiers for v-on. Recall
that modifiers are directive postfixes denoted by a dot.
<!-- the click event's propagation will be stopped -->
<a #click.stop="doThis"></a>
<!-- the submit event will no longer reload the page -->
<form #submit.prevent="onSubmit"></form>
<!-- modifiers can be chained -->
<a #click.stop.prevent="doThat"></a>
<!-- just the modifier -->
<form #submit.prevent></form>
<!-- use capture mode when adding the event listener -->
<!-- i.e. an event targeting an inner element is handled here before being handled by that element -->
<div #click.capture="doThis">...</div>
<!-- only trigger handler if event.target is the element itself -->
<!-- i.e. not from a child element -->
<div #click.self="doThat">...</div>
Another option:
Sometimes we also need to access the original DOM event in an inline statement handler. You can pass it into a method using the special $event variable:
<button #click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
// ...
methods: {
warn: function (message, event) {
// now we have access to the native event
if (event) {
event.preventDefault()
}
alert(message)
}
}
Cheers :)
Didn't quite understand #Armin Ayari's answer, for instance why the code would have to be in the methods object? Anyway in Vue this is what worked for me:
<form ref="form" #submit.prevent="myMethod">
<button type="submit"></button>
</form>
This blocked the page from refreshing and called myMethod instead.
You don't even need the ref. Understood this is an old question, but I found myself here after debugging, and found my form tags were simply mis-placed.
I don't know if I understood your question correctly but you can prevent the default behavior of your form like this:
this.$refs.form.addEventListener("submit", (event) => {
event.preventDefault()
});
Maybe this can help you:
new Vue({
el: '#app',
data: {},
methods: {
submit () {
this.$refs.form.addEventListener('submit', event => {
event.preventDefault()
})
},
alert () {
alert('hello')
}
}
})
<body>
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id='app'>
<div class="form-wrapper" #click='submit'>
<form ref='form' #submit='alert'>
<input type="text">
<button type='submit'>Submit</button>
</form>
</div>
</div>
</body>
I think it's dona help!!
<form method="POST" action="http::localhost:8080/" #submit.prevent="submit_login($event)">
// enter yours inputs here
</form>
submit_login(e) {
if (true) {
e.target.submit();
},
},
After some proper investigation with not a single answer here being related to the original question.
I have found your solution, however it isn't VueJS specific, referencing this article: Javascript e.preventDefault(); not working on submit()
Answer
Your programmatic way to execute submit this.$refs.form.submit() isn't correct if you want to properly preventDefault() or even run other functions.
You need to run this.$refs.form.requestSubmit(), this replicates the functionality as if you would've had a child <button> run the clicked event.
First, don't use preventDefault method. I will illustrate this problem on jQuery:
$('#myForm').on('submit', function (event) {
// step 1.
// stop current action, prevent submitting
event.preventDefault()
// step 2.
// validate inputs
// some validation code
// step 3.
// everything ok, submit it
this.submit()
})
Where is problem with this code? When you submit this form programatically in step 3., it will be again captured and will stop at step 1. again. So, you will be never able to submit this form. Solution:
$('#myForm').on('submit', function (event) {
// step 1.
// validate inputs
// some validation code
// this code will be always executed
// before this form will be submitted
// step 2.
// then do something like this
// continue submitting form and exit
// this callback with returning true
if (inputsAre === 'ok') return true
// if inputs are not ok, program continues
// with following line, which ends this
// callback with false and form will be not submitted
return false
})
I hope you got the point. So, I think what you need is not the preventDefault method, but return true or return false in your doSomething method called on #submit event.
I am working on a vuejs SPA.
I have a view that shows a list of items and another view that shows details for a specific Item.
when I click the item I switch views using:
this.$router.push('/item/' + event.ItemId );
The data is managed using vuex modules.
I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
Where would be the appropriate place (code/lifecycle) to trigger the (async) retrieval of the data required for rendering the item details view?
I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
One way to achieve this, is to define a state variable, named e.g. isLoading, in the data context of the Vue component. This variable would then be true while the data is retrieved asynchronously. In the template, you can use v-if to display a spinner while loading, and displaying the content after that.
If you are retrieving the data multiple times (refreshing the view), I would move the retrieving code into a method, e.g. called loadData. In the mounted section of the Vue component you then can just initially call this method once.
Here is some example code:
<template>
<div>
<button #click="loadData" :disabled="isLoading">Refresh</button>
<div class="item" v-if="!isLoading">
{{ item }}
</div>
<div class="spinner" v-else>
Loading...
</div>
</div>
</template>
<script>
import HttpService from '#/services/HttpService';
export default {
name: 'item-details',
data () {
return {
isLoading: false,
item: {}
};
},
methods: {
loadData () {
this.isLoading = true;
HttpService.loadData().then(response => {
this.item = response.data;
this.isLoading = false;
}, () => {
this.item = {};
this.isLoading = false;
});
}
},
mounted () {
this.loadData();
}
};
</script>
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
This issue you mentioned occurs if you are not using the HTML5 history mode, but an anchor (#) in the URL instead. If you are just changing the part after the anchor in the URL, the page is not actually refreshed by the browser. The Vue component won't be reloaded in this case and the state is still old. There are basically two ways around this:
You are switching from anchors in the URL to a real URL with the HTML5 history mode, supported by the Vue Router. This requires some back-end configuration, though. The browser then does not have this faulty behavior, because there is no anchor. It will reload the page on every manual URL change.
You can watch the $route object to get notified on every route change. Depending on if the user is changing the part after the anchor, or before, the behavior is different (it also depends where the cursor is, when you hit enter). If the part after the anchor is changed (your actual Vue route), only the component is notified. Otherwise, a full page refresh is made. Here's some example code:
// ...inside a Vue component
watch: {
'$route' (to, from) {
this.loadData();
}
}
A brief description is, basically when i click on a "Submit" button per say, an alert should pop out. So I'm using Simplert for Vue to do that.
The project I'm currently doing is a Single Page Application (SPA) so this is where the problem is. The alert only covers the component that I'm in and does not cover the whole page.
Problem:
Is there a way for the alert to cover the whole page?
As I see it you have at least two options to solve it: one dirty and quick and the other a little bit more design oriented
Option 1
override the css position property of the mask:
from: position: absolute to: position: fixed
Option 2
Globally declare the Simplert component, since probably you will use it a lot
// somewhere in your main/app/bootsrap.js file
Vue.compoment('simplert', require('vue2-simplert'))
Now in your root component or where you have your main <router-view> component (if you are using vue-router):
// root component
<template>
<div>
<simplert :useRadius="true" :useIcon="true" ref="simplert"/>
<router-view></router-view>
</div>
</template>
<script>
export default {
mounted () {
this.$on('trigger:simplert', (data) => this.triggerSimplert(data))
},
methods: {
triggerSimplert (data) {
this.$refs.simplert.openSimplert(data)
}
}
}
</script>
Now in any component you need to trigger the modal simply do:
...
someMethod () {
let obj = {
title: 'Custom Function',
message: 'Click close to trigger custom function',
type: 'info',
onClose: this.onClose
}
this.$emit('trigger:simplert', obj)
}
...
Hope this helps.
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.