KnockoutJS - binding child component's click event in parent's markup - javascript

I am trying to write a reusable component library, and have come up with this issue which is bugging me for a time now.
I have overriden the template engine using the example that is provided. I think my problem is mentioned in his 'A quick note about rewriting templates' paragraph (I am not sure though, my problem might be different). But I don't have any idea how to implement it. Any help is appreciated. Here's the problem:
My template engine basically registers templates given as 'templateName' and 'markUp'. Firstly, I have a product model, which goes as:
var Product = function(img, brand, description){
this.img = img;
this.brand = brand;
this.description = description;
};
Then, I have my parent view as:
{
name: 'productlist-view',
template: '<productlist-view class="product-list">\
<ul data-bind="foreach: {data: productlist, as: \'product\'}">\
<product-box params="parent: $parent, productInfo: product" data-bind="click: getProduct"></product-box>\
</ul>\
</productlist-view>'
}
Now in the viewModel of this productlist-view view, productlist property is defined as an array of Product instances. The product-box component is supposed to create a viewModel and an associated template for each of these Products. The product-box component is registered to knockout using:
ko.components.register('product-box', {
'viewModel': function(params){
this.product = params.product;
this.getProduct = function(){
//Will perform an ajax call here
};
},
'template': '<div class="product-box" data-bind="click: getProduct">\
<img class="product-img fl" data-bind="attr: {src: product.img}"/>\
<p data-bind="text: product.brand"></p>\
<p data-bind="text: product.description"></p>\
</div>'
})
I know, in the code above there are two bindings for the getProduct method. I will come to that. For now, just imagine the one in the productlist-view was not there.
The code above generates an html that goes like:
<productlist-view ....
<ul ....
<product-box ....
<div class="product-box" ....
<img .../>
<p ... />
<p ... />
</div>
</product-box>
<product-box ..... goes on
</ul>
</productlist-view>
In the above-code, the wrapper div in the product-box element is totally unnecessary since it just wraps the element. Moreover, the element is already wrapped within product-box element. So I want to remove the wrapper div. The problem here is that, I need the whole product visual to be clickable, but I cannot bind the click event to the getProduct method from the productlist-view template. When the foreach loop iterates in productlist-view template, the $data points to model of the product, not the viewModel (aka product-box).
How can I set this getProduct method, from the parent view?
Is there anyway to remove that unnecessary wrapper div in product-box?
In other words how can I have a clickable product-box component which goes like:
<productlist-view ....
<ul ....
<product-box data-bind="click: getProduct"....
<img .../>
<p ... />
<p ... />
</product-box>
...
</ul>
</productlist-view>

You can make a custom binding handler that would attach a click binding to the parent, but that strikes me as too clever by half (and violates encapsulation). If your click binding is associated with your component, it makes sense that the div be part of the component.
Instead of using custom product-box tags, you can use a virtual tag and the component binding, so you don't have an extraneous wrapper.

Related

Ember generated action or jquery onclick

I have an ember application which works fine. But user's interaction does some DOM insertion like below...
$(.test).append(<a {{action "getData"}}>Get Data</a>);
The problem is that Ember seems do not recognize that an action "getData" has been added to the DOM. Nothing is happening when I click the element. Any thoughts on this?
Another way I am trying to do is:
//create the element
$(.test).append(<a id="idnum">Get Data</a>);
//make click listener
$('#idnum').click(function(){
console.log("get data");
}
my question is where should i place the code inside the component so the it can listen on the click event. Thanks.
You should do it in Ember way. Try handlebars {{#if}} helper to render an element dynamically.
{{#if canGetData}}
<a {{action "getData"}}>Get Data</a>
{{/if}}
Here you can set the value of the canGetData to true in the controller based on the users action.
The first example can't work because ember does not analythe the Handlebars elements in the DOM, but rather parses your Handlebars template with HTMLBars, which is a full HTML parser, and then renders it manually by inserting elements, not text into the DOM.
However the second example is the way to go if you have to rely on external code that does manual DOM manipulation. And it does work. Checkout this twiddle.
This does work:
this.$('.target').append('<a id="idnum">Get Data</a>');
this.$('#idnum').on('click', () => {
alert('clicked');
});
Just make sure that the DOM is ready. So do it in the didInsertElement hook or after the user clicked a button or so.
Like Lux suggested avoid DOM manipulation. I prefer the following approach,
if it is dynamic then you can consider wrapping DOM element as a new component and use component helper.
find sample twiddle
In application.js
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
linksArray:[ Ember.Object.create({value:'Text to display',routename:'home'}),
Ember.Object.create({value:'Text to display2',routename:'home'})],
actions:{
addItem(){
this.get('linksArray').pushObject(Ember.Object.create({value:'AddedDynamically',routename:'home'}));
}
}
});
in Application.hbs
<h1>Welcome to {{appName}}</h1>
<br>
{{#each linksArray as |item|}}
{{component 'link-to' item.value item.route }}
{{/each}}
<button {{action 'addItem'}}>Add Item</button>
<br>
{{outlet}}
<br>
<br>

specify target action to a ember component

I have a component that is wrapping content defined by another template. I want an action on the template to trigger a method in my surrounding component. Is this possible?
Here is what I have for my template. Note this is shortened for brevity.
{{#drop-down}}
<div class="menu-selector clickable" {{action "toggleDropdown"}}>
</div>
{{/drop-down}}
This is my component:
DropDownComponent = Ember.Component.extend
showDropdown: false
actions:
toggleDropdown: ->
#toggleProperty 'showDropdown'
`export default DropDownComponent`
I can verify that everything else in my component is working. If I put the action in my component that loads this template, it works fine. But that's not where I want it.
So you would like to send an action to particular components. Take a notice that
A component is a custom HTML tag whose behavior you implement using JavaScript and whose appearance you describe using Handlebars templates. They allow you to create reusable controls that can simplify your application's templates.
and
An Ember.Component is a view that is completely isolated.
You are probably using wrong tool here. You should use instead custom view.
App.DropdownView = Ember.View.extend
showDropdown: false
elemendId: 'dropdown'
actions:
toggleDropdown: ->
#toggleProperty 'showDropdown'
return;
{{#view 'dropdown'}}
<div>
<div class="menu-selector clickable" {{action "toggleDropdown"}}>
</div>
{{/view}}
Then you can send an action to view by
Ember.View.views.dropdown.send('toggleDropdown');
Demo: http://jsbin.com/zoqiluluco/1/

AngularJS modal window scope

I have problem with scopes of controllers. I'm using controller as directive and I have code similar to this example:
<div ng-controller="ItemsController as itemCtrl">
<table> .. some data ... </table>
<a ng-click="itemCtrl.createItem()">Create new item</a>
</div>
<div id="create-form" ng-controller="ItemFormController as itemFormCtrl">
<form ng-submit="itemFornCtrl.saveItem()">... form inputs ...</form>
</div>
<div id="edit-items" ng-controller="MultipleItemsEdit as multiEditCtrl">
... table with some data ....
<!-- I need this -->
<a ng-click="itemCtrl.createItem()">Create new item</a>
<!-- -->
</div>
Basically there are 3 isolated scopes. But I need to break this isolation and call methods from one scope on another.
I'm currently using ugly "delegate" kind of hack.
Controllers and their methods are not so interesting, only interesting methods are ItemsController.createItem():
this.createItem = function(dataCollection) {
angular.element( $("#create-form) ).controller().createNewItem(dataCollection);
}
and ItemFormController.createNewItem(dataCollection):
this.createNewItem = function(dataCollection) {
... some initialization ....
$("#add-item").dialog( "open" );
}
I need to call createNewItem method on ItemFormController to show modal box. But I cannot do it directly, so I'm using method createItem which gets the create-form element and its controller and calls createNewItem method on it. It is kind of a delegate. But I don't like it, because I need to call createNewItem from many places of my code and I don't want to populate all my controllers with this kind of delegate methods.
Maybe I could make these delegates on some kind of root controller, but isn't there any better solution?
You can nest the edit controller scope in the list controller scope by simply nesting the divs (move the div with ng-controller="MultipleItemsEdit as multiEditCtrl" into the div with ng-controller="ItemsController as itemCtrl"). That way the you can call the method directly.

Most Angular way to add class on click

I'm building an interface with a lot of toggles to control what data is being filtered in a different part of an App's search results. Here is a codepen of it: Here
Coming from a jQuery/Backbone background, what is the most Angular way of toggling the 'active' state of any/all of the filter items? Essentially, almost any <li> tag presented here is a toggle-able feature.
In jQuery, I would put a listener on the view and wait for any click events to bubble up and toggle an 'active' class on the event.target. I want to do it the best way with Angular.
(Also, this is my first Angular project.. I am probably doing all sorts of things the wrong way. Apologies in advance.)
Edit: To clarify the question, I have an App Interface with 20+ possible filter attributes to control a separate module on the page. Every time someone toggles one of these filter attributes, I want to add/remove an 'active' class. Do I put an 'ng-click="function(...)"' in the ng-repeat for each controller? Or is there an easier way to manage this module-wide behavior (a la event bubbling, like in Backbone/jQuery) ?
Thanks!
You can do something like this:
<section ng-init="active = 'areaFoo'">
<div ng-class="{active:active == 'areaFoo'}" ng-click="active = 'areaFoo'"></div>
<div ng-class="{active:active == 'areaBar'}" ng-click="active = 'areaBar'"></div>
</section>
It will populate $scope.active for you, and is very angular as it leverages existing directives, manages the state on scope, and does not leverage dom api's or events outside of directives. There is really no need to involve the controller here, as its display logic.
Learn more about ng-class here.
Multiple active elements
<section>
<div ng-class="{active:areaFoo}" ng-init="areaFoo = true">
<button ng-click="areaFoo = true">activate</button>
<button ng-click="areaFoo = false">de activate</button>
</div>
<div ng-class="{active:areaBar}" ng-init="areaBar = false">
<button ng-click="areaBar = true">activate</button>
<button ng-click="areaBar = false">de activate</button>
</div>
<div ng-class="{active:areaBar}" ng-init="areaBaz = false">
<button ng-click="areaBaz = true">activate</button>
<button ng-click="areaBaz = false">de activate</button>
</div>
</section>
you could also toggle with something like this ng-click="areaFoo = !areaFoo"
I was able to come up with a solution I'm ok with, for anyone curious you can see a demo Here.
Here are the relevant code snippets:
<li ng-repeat='category in data' ng-class='{active: category.isActive}' ng-click='toggleActive(category)' >
<span class='solr-facets-filter-title'>{{category.catTitle}}</span>
<span class='solr-facets-filter-count'>{{category.catResults}}</span>
</li>
An ng-click calls a method on the Controller, toggleActive(category). The current data model gets sent to the method. In the JS:
$scope.toggleActive = function(category){
category.isActive = !category.isActive;
}
The function returns the opposite of the isActive attribute back to the li in question: an ng-class adds the active class for a truthy state of isActive.
I'm not a huge fan of how I have to adjust the data model with flags for active/inactive states like this, but it ends up working out for the best in this case. I can push those isActive states back to the $scope so that other parts of the App can run queries based on that information.

KnockoutJS how to make different ViewModels work toghether?

Suppose i have a main view that contains
A bookable item
A shopping cart
Both elements are in the same page, each of them has its own VM, like this:
<div id='page'>
<div id='item'>
<span data-bind='text: item().name'></span>
<span data-bind='text: item().price'></span> EUR
<!-- What to bind on this click handler? -->
<button>Add</button>
</div>
<hr>
<div id='cart'>
You have 0 items in your cart.
</div>
</div>
Javascript
function ItemVM() {
var self = this;
self.item = ko.observable({id: 1, name:'test', price: 3.99});
}
function CartVM() {
var self = this;
// Adds an item to cart.
self.add = function(item) {
// Business logic here
}
// And so on, other methods here.
self.remove = function(item) {}
self.checkout = function() {}
}
ko.applyBindings(new ItemVM(), document.getElementById('item'));
ko.applyBindings(new CartVM(), document.getElementById('cart'));
I have 2 questions.
1) How to use a click handler, within the 'item' context, that is defined elsewhere? In other words, how to make the button use CartVM.add() as the click handler?
2) Is there something wrong i am doing in reference to KO or MVVM itself?
Fiddle Here
The idea of the MVVM pattern is to have a view bound to a single viewModel. Then you will have data objects described in the model.
In the situtation you have I tend to favour composition. So if I have a view composed of functional elements I tend to compose the viewModel of these separate elements.
I do find this overall is easier. It's likely the container VM will have some elements that are actually part of the individual page. It's probably hard to follow if there are a lot of individual viewModels that are part of the page. The other issue is that cart is coupled to one container div. In the case of cart this may be acceptable. In other cases your functional component may have view elements which may be hard to contain under one div so it becomes hard to segregate viewModels like this.
I've amended your design with:
Note I add a call to add in the itemVM as shown.
function ItemVM() {
this.add = function(data, e) {
viewModel.CartVM.add(data);
};
}
function VM() {
this.ItemVM = new ItemVM();
this.CartVM = new CartVM();
}
var viewModel = new VM();
ko.applyBinding(viewModel);
fiddle to illustrate this here: http://jsfiddle.net/q8uWW/4/
HTH
An example using my binding convention library, it makes it easy to work with multiple view models. https://github.com/AndersMalmgren/Knockout.BindingConventions
To communicate between models you can use a Event Aggregate pattern, I have one in a library called SignalR.EventAggregatorProxy, if you have no use for SignalR you can extract the eventaggregatorn part. https://github.com/AndersMalmgren/SignalR.EventAggregatorProxy/
The idea with my convention library is to use template bindings for each view model. Like
<div id='page'>
<div id='item' data-name="item"></div>
<hr>
<div id='cart' data-name="cart"></div>
</div>
<script id="ItemView" type="text/html">
<span data-name='name'></span>
<span data-name='price'></span> EUR
<button data-name="add">Add</button>
</script>
<script id="CartView" type="text/html">
You have <span data-name="count"></span> items in your cart.
</script>
Fiddle
http://jsfiddle.net/hL5rY/

Categories