I'm working with Backbone.js for the first time, and trying to get my head around how it works. I have a search form that pulls in results via Ajax and writes them out to the page dynamically.
I'm now trying to figure out how best to structure this in Backbone - I read this SO question, but I don't completely understand how to wire the form and the results together.
Here's my HTML:
<form id="flight-options"> <!-- options for user to choose-->
<input type="radio" name="journey" value="single">Single<br/><input type="radio" name="journey" value="return">Return
<input type="checkbox" name="airline" value="aa">AA<br><input type="checkbox" name="airline" value="united">United
</form>
<div id="results"> <!-- results, written by Ajax -->
<h3>Results</h3>
<ul id="results-list">
</div>
Here's how I'm thinking of structuring the Backbone code:
var SearchModel = Backbone.Model.extend({
performSearch: function(str) {
// fire the ajax request. provide a bound
// _searchComplete as the callback
},
_searchComplete: function(results) {
this.set("searchResults", results);
}
});
var SearchFormView = Backbone.View.extend({
tagName: "form",
id: "flight-options",
events: {
"click input": "getResults"
},
getResults: function() {
// Get values of selected options, use them to construct Ajax query.
// Also toggle 'selected' CSS classes on selected inputs here?
this.model.performSearch(options);
}
});
var SearchResultsView = Backbone.View.extend({
tagName: "ul",
id: "results-list",
initialize: function() {
this.model.on("change:searchResults", this.displayResults, this);
},
displayResults: function(model, results) {
//append results to results-list here.
//update contents of blurb here?
}
});
var searchModel = new SearchModel();
var searchFormView = new SearchFormView({ model: searchModel });
var searchResultsView = new SearchResultsView({ model: searchModel });
My questions:
Is this basically a sensible structure to use, with one view for the form and one for the results - the first view updating the model, the second view watching the model?
I also want to update the contents of the <h3> results header when there are new results - where is the most sensible place to do this, in the above code?
I want to toggle the selected class on an input when the user clicks on a form input - where is the logical place to do this, within the above code?
Thanks for your help.
Yes, that's a logical organization and a great way to use Backbone Views.
You could approach this a couple ways:
Have a separate View for the title (e.g. SearchResultsTitleView) that also listens for changes on the model. That seems a bit excessive to me.
Have your SearchResultsView update both the title <h3> and results <ul>. Instead of binding itself to the #results-list <ul>, it might bind to the #results <div> and have two functions, one for updating each child.
That would seem like the responsibility of the SearchFormView, either listening for changes on the model or simply updating the state when the event occurs.
Related
I have created my own widget to be called in tree views this way:
<field name="selected" widget="toggle_switch"/>
The field selected is of type boolean. I created the widget class extending the Column class:
var ListView = require('web.ListView');
var ToggleSwitch = ListView.Column.extend({
template: 'ToggleSwitchSheet',
events: {
'click .slider': 'on_click',
},
init: function() {
this._super.apply(this, arguments);
},
_format: function(row_data, options) {
return QWeb.render(this.template, {
'widget': this,
'row_data': row_data,
'prefix': session.prefix,
});
},
})
And I have registered it this way:
var core = require('web.core');
core.list_widget_registry.add('field.toggle_switch', ToggleSwitch);
The code of the template:
<t t-name="ToggleSwitchSheet">
<label class="switch">
<t t-if="row_data['selected']['value']">
<input type="checkbox" checked="checked"/>
</t>
<t t-if="!row_data['selected']['value']">
<input type="checkbox"/>
</t>
<span class="slider round"></span>
</label>
</t>
And it is working, but now I want to modify the value of the selected field each time the user clicks on the main element of the template I made for the widget.
The problem is that I am not able to do that. It seems that events dictionary is not available for the Column class, and I cannot use something like this.on('click', this, this.on_click); or this.$el.find(...), as this brings only the field data.
Can anyone help me? Should I inherit from other classes to use events in my widget (in fact I have tried, but in every case my Qweb template just disappeared from the tree view...)?
I think that you are mixin things here. Or maybe not. Just to be clear Column widgets are intended only to show information. For example to give your personalized html widget to fit properly into the list view. To execute actions there are action buttons that you could use to change the model record value in a python method.
I know that it's not exactly the same but I'm just setting the bases that you could make your custom widget clickable by using a button in your column with your custom widget associated rendering the result of the checked value inside the button, allowing you to make calls to custom methods in your model to change the record value.
That been said your widget is almost the same of the ColumnBoolean widget but if you wanna continue with the work done I think that you could do it like this:
odoo.define('listview_label_click', function (require) {
"use strict";
var ListView = require('web.ListView');
ListView.List.include({
init: function (group, opts) {
this._super.apply(this, arguments);
this.$current.delegate('td label.switch', 'click', function (e) {
e.stopPropagation();
// execute your code here, like:
var checked = $(e.currentTarget).find('input').prop('checked');
});
}
});
});
I have a customer who is a member of a web site. He has to fill a form every time which is really very often. That's why he wants me to develop an application for him to make this process automatic. When I use the webBrowser control to manipulate it, I am able to login but after that there are fields that contains data-binding. These fields are the ones I need to manipulate. When I push the data to necessary fields, it's not working, because in the html tag, there is no value attribute, instead it has data-binding. So my question is how can I manipulate and push data to these fields?
Thank you so much for your all help in advance.
Knockout uses data-binds to listen to changes in an input and update an underlying model. For example, the value binding listens to change events and writes the new value to a data-bound observable.
If you update a value attribute through code, the change event isn't triggered. You'll see the new value in the UI, but the javascript model won't be updated.
You can combat this by explicitly triggering a change. Here's an example:
Type in the input: you'll see a console.log that shows knockout gets updated
Press the button to inject a new value: you won't see a log: knockout isn't updated
Press the last button to trigger a change event. You'll notice knockout now updates the model.
Of course, you can combine the two click listeners into one function. I've separated them to get the point across.
// Hidden knockout code:
(function() {
var label = ko.observable("test");
label.subscribe(console.log.bind(console));
ko.applyBindings({ label: label });
}());
// Your code
var buttons = document.querySelectorAll("button");
var input = document.querySelector("input");
buttons[0].addEventListener("click", function() {
input.value = "generated value";
});
buttons[1].addEventListener("click", function() {
// http://stackoverflow.com/a/2856602/3297291
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
input.dispatchEvent(evt);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" data-bind="value: label">
<button>inject value from outside</button>
<button>let knockout know something changed</button>
There is a Bckabone view Product:
Product = Backbone.View.extend({
templateBasic: _.template($("#pcard-basic").html()),
templateFull: _.template($("#pcard-full").html()),
initialize: function() {
this.render(this.templateBasic);
},
// ...
Here's my draft: http://jsfiddle.net/challenger/xQkeP/73
How do I hide/show other views when one of them gets chosen/unchosen to view its full template so it could expand to a full container width.
Should I use a view for an entire collection? How do I deal with event handling?
Thanks!
EDIT
That's my final draft: http://jsfiddle.net/challenger/xQkeP/
But still I'm not sure whether I could achieve the same result in more elegant manner? I just think that hiding siblings is not the best way of resolving it:
viewBasic: function(e) {
e.preventDefault();
this.render(this.templateBasic);
if(this.switchedToFull) {
this.$el.siblings().show();
this.switchedToFull = false;
}
},
viewFull: function(e) {
e.preventDefault();
this.render(this.templateFull);
this.$el.siblings().hide();
this.switchedToFull = true;
}
If I understand correctly you want to display all your models inside your collections in two different ways, leaving the choice of how to present them to the user, right?
One way you can do this is to create a main view where the user choice is made. When the user decides you should trigger a method from that view that renders every model from the collection using a different template. On your main view you should have a container (table, div, ul, etc) where you'll append each of the model view.
So, in the end, you have to views. One acting as a container for the collection that takes care of handling the users choice. Then you have another view to render a single model from the collection. This view has to templates that can be used. On the main view you iterate over the collection creating a new view instance for each model to append in the container using a different template depending on the user decision.
I would like to implement knockout functionality to an already existing page. Therefore I want to manipulate with the ViewModel data from existing script. I have mocked up an example but it doesn't work correctly.
A ViewModel is correctly bound to the UI (try typing in the input). It functions also when I modify the ViewModel data in backend (by pressing a button). However when I modify the input value again by typing into the input box, the data is not changed.
How can I propperly modify ViewModel data from backend (some existing code manipulates the data). Please note, that I have used jQuery click event as an example. Existing code may manipulate the data in the different manner.
Here is the code (HTML):
<!-- View -->
<p>First name: <strong data-bind="text: myName"></strong></p>
<input data-bind="value: myName"></input>
<button>Click me</button>
And JS:
// ViewModel
var AppViewModel = function() {
this.myName= ko.observable("John Doe");
}
// ViewModel instance
var app = new AppViewModel();
// Activates knockout.js
ko.applyBindings(app);
// Custom external code that changes the data in the ViewModel instance
$("button").click(function() {
app.myName= ko.observable("Steve Peterson");
ko.applyBindings(app);
});
Instead of creating new observable in click handler
app.myName= ko.observable("Steve Peterson");
modify value of existing observable
app.myName("Steve Peterson");
Working example
http://jsfiddle.net/S9HBq/
I have a many-to-many relationship with two of my backbone.js models implemented using a pivot table on the serverside. I'm trying to figure out how to structure it clientside. My current structure is:
1) I have a Tag model, and a TagView which renders a checkbox and the tag label, I have a checked event on the checkbox which does nothing at the moment.
I have a TagsCollection, which holds a bunch of Tags.
I have a TagsCollectionView, which binds add, reset etc of the TagsCollection, and adds TagViews for the added Tags, renders them, and appends the html to its current html (on reset, the html is reset).
I have a global TagCollection instance which contains all the possible tags
I have a Notes Model which contains an (empty) TagCollection called selectedtags on init.
The server returns an array of selected tagids for each Notes, which I add to its TagCollection.
Now comes the hard part, tying it all together.. my NotesView has its own TagsCollectionView which is bound to the global TagsCollection (so it can list all the Tags).. now, how do I get a checked event on the checkedbox of its sub TagViews to trigger an add to this Notes model's selectedtags? Should I provide a reference to the this Notes model instance to the TagsCollectionView on init which then provides it to all the TagViews it creates, whose checked event then adds/removes items from that model? That's the best way I can figure out how to do this, any other thoughts would be appreciated.
View is only for visual display of model. Please specify the need for TagsCollectionView in more details:
Use change event for checking the checkbox.
I would advise incremental coding. First work with the Tag and TagView, As it works, continue adding collection to hold the Tags. After that you can add Notes. It's a 'divide and conquer' :)
Don't confuse with the whole design, it is very simple when you start.
I can not provide the whole design due to lack of requirement details. but, I think below code should trigger the starting point of your design.
var TagsCollectionView=Backbone.View.extend({
el:$(),
});
var Tag=Backbone.Model.extend({
defaults:{
// define the default properties
}
});
var TagView=Backbone.View.extend({
el:$("body"),
events:{
'change #checkBox':'customFunction'
},
initialize: function(){
_.bindAll(this, 'render','customFunction');
this.render();
},
render:function(){
var tag=new Tag();
// code to render the checkbox and label
},
customFunction:function(){
// whatever you want to do after checking event of checkbox
}
});
// collection to collect all the tags
var TagCollection=Backbone.Collection.extend({
model:Tag
});
var Notes=Backbone.Model.extend({
defaults:{
'tagCollection':TagCollection
}
});
// you do not require TagsCollectionView