Best practices when editing form with emberjs - forms

Is there any good solutions when editing a form to save the model only if the user clicked on the save button and retrieve the old datas if the user canceled the action ?
I've seen some solutions like duplicating the object that is data-binded with each form fields and set the the initial object with the duplicated one when it is saved.
If you could give answers without using ember data could be great.

I understand you would prefer a solution that doesn't use ember-data, but I would argue that using ember-data is best practices. Here is a solution using ember-data because I imagine a lot of people may come across this question...
If you set up your route as follows, it will do exactly that.
App.CommentEditRoute = Em.Route.extend({
model: function(params) {
return this.store.find('comment', params.comment_id);
},
actions: {
willTransition: function(transition) {
var model = this.get('controller.content');
if (model.get('isDirty')) {
model.rollback();
}
}
},
});
If you call this.get('content').save() in the controller (because the user clicked the save button) it will persist the changes through the adapter and isDirty will be set to false. Thus, the model will not rollback. Otherwise, if you did not call this.get('content').save() in the controller, the isDirty property will be true and the unsaved changes will be discarded. See the DS.Model docs for more info.
willTransition is an event automatically called when the route is about to change - you don't have to call it directly.
Your controller might look like this:
App.CommentEditController = Em.ObjectController.extend({
save: function() {
var _this = this;
_this.get('content').save().then(function() {
// Success
_this.transitionToRoute('comments');
}, function() {
// Failure
Em.assert('Uh oh!');
});
},
cancel: function() {
this.transitionToRoute('comments');
},
});
Also, be sure to utilize the default HTML form submission using a proper HTML button or input for submission so you can capture the submission event in your view as follows:
App.CommentEditView = Em.View.extend({
submit: function() {
this.get('controller').save();
},
});

Related

How to properly use the new asynchronous fragment loading

OpenUI5 newbie here. I am trying to use OpenUI5 fragments, much like shown in the Walkthrough example, Step 16, in the documentation. I have a problem seeing how this can work properly.
The code below is a copy and paste from the Walkthrough example, Step 16, in the documentation:
onOpenDialog : function () {
var oView = this.getView();
// create dialog lazily
if (!this.byId("helloDialog")) {
// load asynchronous XML fragment
Fragment.load({
id: oView.getId(),
name: "sap.ui.demo.walkthrough.view.HelloDialog"
}).then(function (oDialog) {
// connect dialog to the root view of this component
oView.addDependent(oDialog);
oDialog.open();
});
} else {
this.byId("helloDialog").open();
}
}
Since the HelloDialog fragment is loaded asynchronously, it is clear that the onOpenDialog function may return control to the user before the dialog has been created and opened. It is in the nature of asynchronous calls that we must not make any assumptions about how long it will take until the asynchronous code is executed. Anything is possible. Therefore, we must allow for the possibility that the user has control over the web page for any amount of time before the dialog shows up, say, several seconds. What is the user going to do? They're going to click the button for opening the dialog again, and again, and again, thereby creating the dialog multiple times, subverting the intended logic of the code. To be honest, I am not sure if I would be comfortable having that kind of thing in my code. How should I deal with this?
in general, you're right, the dialog could take time to load but usually, the process takes a fraction of time and the user should not see any "loading" time.
This is possible only if your dialog load data asynchronously too.
If you really want to be sure to give graphical feedback to the user you could do like this:
onOpenDialog : function () {
var oView = this.getView();
// create dialog lazily
if (!this.byId("helloDialog")) {]
// set the view to busy state
oView.setBusy(true);
// load asynchronous XML fragment
Fragment.load({
id: oView.getId(),
name: "sap.ui.demo.walkthrough.view.HelloDialog"
}).then(function (oDialog) {
// remove the busy state
oView.setBusy(false);
// connect dialog to the root view of this component (models, lifecycle)
oView.addDependent(oDialog);
oDialog.open();
});
} else {
this.byId("helloDialog").open();
}
}

How to seperate Vue logic in a laravel app based on layout and page templates

I have a laravel app and a Vue instance attached to the body (or a div, just inside the body).
const app = new Vue({
el: '#app'
});
I think it makes sense to use the Vue instance for stuff relating to the layout (eg header, nav, footer logic).
Now I have a form that is visible on a specific route (e.g. example.com/thing/create). I want to add some logic to it, e.g. hiding a field based on selected option in the form. It is logic meant for just this form (not to be reused). I prefer not to put all the logic inline with the form but put it in the app.js. I could put it in the Vue instance bound to the body but that sounds odd as it only applies to the form that is much deeper into the dom.
I want to leave the markup of the form in the blade template (that inherits the layout).
I tried creating a component but am not sure how to bind this inside the main Vue instance. What is the best way to handle things for this form, put it in the app.js and have it somewhat structured, putting the variables somewhat into scope. Or is it really necessary to remove the main Vue instance bound to the full layout code?
What I tried was something like this, but it does not work (attaching it to the <form id="object-form"> seems to fail:
var ObjectForm = {
template: function() { return '#object-form'},
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
}
};
Things do work if I remove the #app Vue instance or when I put everything directly in the app Vue instance. But that seems messy, if I have similar variables for another form they should be seperated somewhat.
I would appreciate some advice regarding the structure (differentiate page layout and page specific forms) and if possible some example to put the form logic inside the main app.js.
I hope this helps kind of break things down for you and helps you understand Vue templating.
It is best to take advantage of Vue's components. For you it would look something like this. Some of this code depends on your file structure, but you should be able to understand it.
In your app.js file (or your main js file)
Vue.component('myform',require('./components/MyForm.vue'));
const app = new Vue({
el: "#app"
});
Then create the MyForm.vue file
<template>
<form>
Put Your Form Markup Here
</form>
</template>
<script>
// Here is where you would handle the form scripts.
// Use methods, props, data to help set up your component
module.exports = {
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
},
methods: {
// add methods for dynamically changing form values
}
}
</script>
Then you will be able to just call in your blade file.
<myform></myform>
I found out how to do it. The trick was to use an inline template. Surround the form in the view with:
<object-form inline-template>
<form>...</form>
</object-form>
Where object-form is the name of the component. In the ObjectForm code above I remove the template, like this:
var ObjectForm = {
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
}
};
I attach the component within the root vue app like this:
const app = new Vue({
el: 'body',
components: {
'object-form': ObjectForm
}
});
This way I can use the form as it was generated from the controller and view and I can separate it from the root (attached to body) methods and properties.
To organize it even better I can probably store the ObjectForm in a seperate .vue file the way #Tayler Foster suggested.

Protractor element handling

I have a question regarding how protractor handles the locating of elements.
I am using page-objects just like I did in Webdriver.
The big difference with Webdriver is that locating the element only happens when a function is called on that element.
When using page-objects, it is advised to instantiate your objects before your tests. But then I was wondering, if you instantiate your object and the page changes, what happens to the state of the elements?
I shall demonstrate with an example
it('Change service', function() {
servicePage.clickChangeService();
serviceForm.selectService(1);
serviceForm.save();
expect(servicePage.getService()).toMatch('\bNo service\b');
});
When debugging servicePage.getService() returns undefined.
Is this because serviceForm is another page and the state of servicePage has been changed?
This is my pageobject:
var servicePage = function() {
this.changeServiceLink = element(by.id('serviceLink'));
this.service = element(by.id('service'));
this.clickChangeService = function() {
this.changeServiceLink.click();
};
this.getService = function() {
return this.service.getAttribute('value');
};
};
module.exports = servicePage;
Thank you in advance.
Regards
Essentially, element() is an 'elementFinder' which doesn't do any work unless you call some action like getAttribute().
So you can think of element(by.id('service')) as a placeholder.
When you want to actually find the element and do some action, then you combine it like element(by.id('service')).getAttribute('value'), but this in itself isn't the value that you are looking for, it's a promise to get the value. You can read all about how to deal with promises elsewhere.
The other thing that protractor does specifically is to patch in a waitForAngular() when it applies an action so that it will wait for any outstanding http calls and timeouts before actually going out to find the element and apply the action. So when you call .getAttribute() it really looks like
return browser.waitForAngular().then(function() {
return element(by.id('service')).getAttribute('value');
});
So, in your example, if your angular pages aren't set up correctly or depending on the controls you are using, you might be trying to get the value before the page has settled with the new value in the element.
To debug your example you should be doing something like
it('Change service', function() {
servicePage.getService().then(function(originalService) {
console.log('originalService: ' + originalService);
});
servicePage.clickChangeService();
serviceForm.selectService(1);
serviceForm.save();
servicePage.getService().then(function(newService) {
console.log('newService: ' + newService);
});
expect(servicePage.getService()).toMatch('\bNo service\b');
});
The other thing that I'm seeing is that your pageObject appears to be a constructor when you could just use an object instead:
// name this file servicePage.js, and use as 'var servicePage = require('./servicePage.js');'
module.exports = {
changeServiceLink: element(by.id('serviceLink')),
service: element(by.id('service')),
clickChangeService: function() {
this.changeServiceLink.click();
},
getService: function() {
return this.service.getAttribute('value');
}
};
Otherwise you would have to do something like module.exports = new servicePage(); or instantiate it in your test file.
When you navigate another page, the web elements will be clear, that you selected. So you have to select again. You can select all elements that is in a page of HTML. You can click that you see. So the protactor + Selenium can decide what is displayed.
You have a mistake in your code, try this:
expect(servicePage.getService()).toMatch('\bNo service\b');

How to observe added events in Meteor without firing on initialization?

There is a lot of information on this topic out there, but I can't seem to get it working for myself. I am using toastr to display notifications of events to the user in the top right hand corner of my app. I need to add an observer for the 'added' event to a collection, and create the toastr notification when an item is added. The problem is that the observer fires when the collection is initialized. I've tried about a half dozen different ways trying to check of the collection is ready() before I allow the observer code to continue through and show notifications, but I can't get it working consistently, especially when changing pages. Here is some sample code:
MainController = RouteController.extend({
before: [
function() {
deviceEventsInitializing = true;
var alerts = this.subscribe("alerts", Meteor.user()._id);
if (alerts.ready()) {
deviceEventsInitializing = false;
}
Alerts.find().observeChanges({
added: function(id, doc) {
if (deviceEventsInitializing || deviceEventsInitializing == undefined) {
return;
}
doToastrStuff();
}
});
this.next();
}
],
});
This is just my latest attempt. The flow goes like this:
1.) Subscription happens, all the items in the collection hit the observer but deviceEventsInitializing is true so it does nothing.
2.) alerts.ready() fires and deviceEventsInitilizing is set to false.
3.) The added trigger fires again for all the events in the collection, causing toastr to be called for every item.
All I'm interested in is the following:
1.) Some trigger or event where I can set a variable that says the subscription is reloading the collection.
2.) Some trigger or event that tells me that this reloading of the collection is complete so I can set a variable indicating that.
I think you're on the right track, but trying to manage reactivity like this using solely IronRouter can be a nightmare. I've tried and failed before.
Instead, leverage Mongo to limit your reactivity to only alerts you care about. Let's imagine your alerts database looks something like:
{
_id: 1,
hasNotified: false,
...
}
Now, Mongo is deciding what is new vs. not new instead of trying to determine state based on IronRouter timing. In fact, because in Meteor any alerts cursor is natively reactive, you don't even need to observeChanges:
MainController = RouteController.extend({
waitOn: function() {
return this.subscribe("alerts", Meteor.userId());
},
data: function() {
var newAlerts = Alerts.find({hasNotified: false}).forEach(function(doc) {
doToastrStuff();
Alerts.update({_id: doc._id}, {$set: {hasNotified: true}});
});
}
})
With this kind architecture, navigating to other routes, reloading the page, etc. will not re-fire any of your alerts because Mongo stores your alert state.

jquery selection with .not()

I have some troubles with jQuery.
I have a set of Divs with .square classes. Only one of them is supposed to have an .active class. This .active class may be activated/de-activated onClick.
Here is my code :
jQuery().ready(function() {
$(".square").not(".active").click(function() {
//initialize
$('.square').removeClass('active');
//activation
$(this).addClass('active');
// some action here...
});
$('.square.active').click(function() {
$(this).removeClass('active');
});
});
My problem is that the first function si called, even if I click on an active .square, as if the selector was not working. In fact, this seems to be due to the addClass('active') line...
Would you have an idea how to fix this ?
Thanks
Just to give something different from the other answers. Lonesomeday is correct in saying the function is bound to whatever they are at the start. This doesn't change.
The following code uses the live method of jQuery to keep on top of things. Live will always handle whatever the selector is referencing so it continually updates if you change your class. You can also dynamically add new divs with the square class and they will automatically have the handler too.
$(".square:not(.active)").live('click', function() {
$('.square').removeClass('active');
$(this).addClass('active');
});
$('.square.active').live('click', function() {
$(this).removeClass('active');
});
Example working: http://jsfiddle.net/jonathon/mxY3Y/
Note: I'm not saying this is how I would do it (depends exactly on your requirement) but it is just another way to look at things.
This is because the function is bound to elements that don't have the active class when you create them. You should bind to all .square elements and take differing actions depending on whether the element has the class active:
$(document).ready(function(){
$('.square').click(function(){
var clicked = $(this);
if (clicked.hasClass('active')) {
clicked.removeClass('active');
} else {
$('.square').removeClass('active');
clicked.addClass('active');
}
});
});