I'm looking into breaking a large UI component into smaller pieces, but one thing I'm not entirely sure how to handle and something that I can't seem to find general info about is delegating events from sub-components.
Lets say, for example, you have a todo list and clicking on a todo list will update a sidebar with details about the todo. Right now the code we have is basically 1 file with a template and does all the events for everything. It searches in DOM nodes for data when you click on the delegated handler attached to the wrapper of the list and sidebar. There is potentially thousands of clickable rows.
I'd like something instead that is along the lines of this (this is just pseudo code):
app.controllers.todos = library.controller.extend({
init: function () {
// Store all the todo items in an array
this.todoItems = [];
todoItemsReturnedFromServer.forEach(function (data) {
var todoItem = new app.todo.item(data);
todoItems.push(todoItem);
});
this.todoList = new app.todo.list({data: this.todoItems}); // start with initial data
this.sidebar = new app.sidebar();
},
render: function () {
$('#wrapper').append(this.todoList.render());
$('#sidebar').append(this.sidebar.render());
}
});
So, you'd have a todoList component you could add/remove from and you could have events hooked up which could update the DOM, but is decoupled from the data (i.e. you could not have any DOM and it'd work). Now, in our app, if you click on a todoItem, the todoItem needs to delegate it's event to todoList or higher. I don't want to have 1 click event for every todoItem.
My only idea is to have a "delegate" property on the sub component that the parent takes (if supported) and creates events from. It'd have a hash of events and handlers. If the event handler is already attached it simply ignores it.
Are there other examples or patterns for this type of thing?
Have you tried to use a client-side MVC framework? These are created to solve such problems. I would suggest starting with backbone.js.
Here is a great introductory book. It deals with nested views and models, too:
http://addyosmani.github.io/backbone-fundamentals/#working-with-nested-views
http://addyosmani.github.io/backbone-fundamentals/#managing-models-in-nested-views
http://addyosmani.github.io/backbone-fundamentals/#working-with-nested-models-or-collections
Related
I'm trying to use native web components for one of my UI project and for this project, I'm not using any frameworks or libraries like Polymer etc. I would like to know is there any best way or other way to communicate between two web components like we do in angularjs/angular (like the message bus concept).
Currently in UI web-components, I'm using dispatchevent for publishing data and for receiving data, I'm using addeventlistener.
For example, there are 2 web-components, ChatForm and ChatHistory.
// chatform webcomponent on submit text, publish chattext data
this.dispatchEvent(new CustomEvent('chatText', {detail: chattext}));
// chathistory webcomponent, receive chattext data and append it to chat list
this.chatFormEle.addEventListener('chatText', (v) => {console.log(v.detail);});
Please let me know what other ways work for this purpose. Any good library like postaljs etc. that can easily integrate with native UI web components.
If you look at Web Components as being like built in components like <div> and <audio> then you can answer your own question. The components do not talk to each other.
Once you start allowing components to talk directly to each other then you don't really have components you have a system that is tied together and you can not use Component A without Component B. This is tied too tightly together.
Instead, inside the parent code that owns the two components, you add code that allows you to receive events from component A and call functions or set parameters in Component B, and the other way around.
Having said that there are two exceptions to this rule with built in components:
The <label> tag: It uses the for attribute to take in an ID of another component and, if set and valid, then it passes focus on to the other component when you click on the <label>
The <form> tag: This looks for form elements that are children to gather the data needed to post the form.
But both of these are still not TIED to anything. The <label> is told the recipient of the focus event and only passes it along if the ID is set and valid or to the first form element as a child. And the <form> element does not care what child elements exist or how many it just goes through all of its descendants finding elements that are form elements and grabs their value property.
But as a general rule you should avoid having one sibling component talk directly to another sibling. The methods of cross communications in the two examples above are probably the only exceptions.
Instead your parent code should listen for events and call functions or set properties.
Yes, you can wrap that functionality in an new, parent, component, but please save yourself a ton of grief and avoid spaghetti code.
As a general rule I never allow siblings elements to talk to each other and the only way they can talk to their parents is through events. Parents can talk directly to their children through attributes, properties and functions. But it should be avoided in all other conditions.
Working example
In your parent code (html/css) you should subscribe to events emited by <chat-form> and send event data to <chat-history> by execute its methods (add in below example)
// WEB COMPONENT 1: chat-form
customElements.define('chat-form', class extends HTMLElement {
connectedCallback() {
this.innerHTML = `Form<br><input id="msg" value="abc"/>
<button id="btn">send</button>`;
btn.onclick = () => {
// alternative to below code
// use this.onsend() or non recommended eval(this.getAttribute('onsend'))
this.dispatchEvent(new CustomEvent('send',{detail: {message: msg.value} }))
msg.value = '';
}
}
})
// WEB COMPONENT 2: chat-history
customElements.define('chat-history', class extends HTMLElement {
add(msg) {
let s = ""
this.messages = [...(this.messages || []), msg];
for (let m of this.messages) s += `<li>${m}</li>`
this.innerHTML = `<div><br>History<ul>${s}</ul></div>`
}
})
// -----------------
// PARENT CODE
// (e.g. in index.html which use above two WebComponents)
// Parent must just subscribe chat-form send event, and when
// receive message then it shoud give it to chat-history add method
// -----------------
myChatForm.addEventListener('send', e => {
myChatHistory.add(e.detail.message)
});
body {background: white}
<h3>Hello!</h3>
<chat-form id="myChatForm"></chat-form>
<div>Type something</div>
<chat-history id="myChatHistory"></chat-history>
+1 for both other answers, Events are the best because then Components are loosly
coupled
Also see: https://pm.dartus.fr/blog/a-complete-guide-on-shadow-dom-and-event-propagation/
Note that in the detail of a Custom Event you can send anything you want.
Event driven function execution:
So I use (psuedo code):
Elements that define a Solitaire/Freecell game:
-> game Element
-> pile Element
-> slot Element
-> card element
-> pile Element
-> slot Element
-> empty
When a card (dragged by the user) needs to be moved to another pile,
it sends an Event (bubbling up the DOM to the game element)
//triggered by .dragend Event
card.say(___FINDSLOT___, {
id,
reply: slot => card.move(slot)
});
Note: reply is a function definition
Because all piles where told to listen for ___FINDSLOT___ Events at the game element ...
pile.on(game, ___FINDSLOT___, evt => {
let foundslot = pile.free(evt.detail.id);
if (foundslot.length) evt.detail.reply(foundslot[0]);
});
Only the one pile matching the evt.detail.id responds:
!!! by executing the function card sent in evt.detail.reply
And getting technical: The function executes in pile scope!
(the above code is pseudo code!)
Why?!
Might seem complex;
The important part is that the pile element is NOT coupled to the .move() method in the card element.
The only coupling is the name of the Event: ___FINDSLOT___ !!!
That means card is always in control, and the same Event(Name) can be used for:
Where can a card go to?
What is the best location?
Which card in the river pile makes a Full-House?
...
In my E-lements code pile isn't coupled to evt.detail.id either,
CustomEvents only send functions
.say() and .on() are my custom methods (on every element) for dispatchEvent and addEventListener
I now have a handfull of E-lements that can be used to create any card game
No need for any libraries, write your own 'Message Bus'
My element.on() method is only a few lines of code wrapped around the addEventListener function, so they can easily be removed:
$Element_addEventListener(
name,
func,
options = {}
) {
let BigBrotherFunc = evt => { // wrap every Listener function
if (evt.detail && evt.detail.reply) {
el.warn(`can catch ALL replies '${evt.type}' here`, evt);
}
func(evt);
}
el.addEventListener(name, BigBrotherFunc, options);
return [name, () => el.removeEventListener(name, BigBrotherFunc)];
},
on(
//!! no parameter defintions, because function uses ...arguments
) {
let args = [...arguments]; // get arguments array
let target = el; // default target is current element
if (args[0] instanceof HTMLElement) target = args.shift(); // if first element is another element, take it out the args array
args[0] = ___eventName(args[0]) || args[0]; // proces eventNR
$Element_ListenersArray.push(target.$Element_addEventListener(...args));
},
.say( ) is a oneliner:
say(
eventNR,
detail, //todo some default something here ??
options = {
detail,
bubbles: 1, // event bubbles UP the DOM
composed: 1, // !!! required so Event bubbles through the shadowDOM boundaries
}
) {
el.dispatchEvent(new CustomEvent(___eventName(eventNR) || eventNR, options));
},
Custom Events is the best solution if you want to deal with loosely coupled custom elements.
On the contrary if one custom element know the other by its reference, it can invoke its custom property or method:
//in chatForm element
chatHistory.attachedForm = this
chatHistory.addMessage( message )
chatHistory.api.addMessage( message )
In the last example above communication is done through a dedecated object exposed via the api property.
You could also use a mix of Events (in one way) and Methods (in the other way) depending on how custom elements are linked.
Lastly in some situations where messages are basic, you could communicate (string) data via HTML attributes:
chatHistory.setAttributes( 'chat', 'active' )
chatHistory.dataset.username = `$(this.name)`
I faced the same issue and as I couldn't find any fitting library I decided to write one on my own.
So here you go: https://www.npmjs.com/package/seawasp
SeaWasp is a WebRTC data layer which allows communication between components (or frameworks etc).
You simply import it, register a connection (aka tentacle ;) ) and you can send and receive messages.
I'm actively working on it so if you have any feedback /needed features, just tell me :).
For the case where the parent and child know about each other, like in a toaster example.
<toaster-host>
<toast-msg show-for='5s'>Success</toast-msg>
</toaster-host>
Lots of options but for:
Parent passing data to the child -> attributes or observedAttributes for primitives. If complex objects need to be passed either expose a function or a property aka domProperty that can be set. If a domProperty needs to react to being updated, it can be wrapped in a proxy.
Child passing data to parent -> can use events, or can query for the parent using .closest('toaster-host') and call a function or set a property. I prefer to query and call a function. Typescript helps with this type of approach.
In cases like the toaster example, the toaster-host and the toast-item will always be used together, so the argument about loose coupling is academic at best. They are different elements mainly because they have different jobs. If you wanted to swap out implementations of the toast-msg you could do that when you define the custom element, or even by changing the import statement to point to a different file.
I'm trying to understand whether is necessary to unbind an event that is binded on the current instance of a view.
For instance when I do:
$(this.el).on('click', callback);
is it necessary to unbind the events (e.g. using off() or $(this).unbind('click') inside the callback function) or maybe the view will destroy the event and will give it to garbage collector?
You should setup all of your events via the View's events hash and only unbind them when removing a view via .remove().
Backbone Views use Event Delegation for the DOM Event handlers, so you can set up events for View HTML that doesn't exist yet, and, once the HTML is generated, the event handlers will catch the events as expected. All events handlers are attached to the views root element and watch for specific events that occur within that element or it's children.
Events will be unbound when you remove the view via Backbone.View.remove()
view.remove();
If you need to unbind events while the view is displayed (not common), you can specifically unbind that event via jQuery's .off(), but, you shouldn't have to (or want to) manage binding/unbinding your events.
The problem with manually unbinding events is that you may/probably will quickly find yourself conditionally unbinding and binding these event handlers according to user input. You'll go down the path of "unbind an event here, rebind it here, but unbind when this condition is true or this one is false"... it gets confusing, fragile and unmaintainable very quickly.
Instead, you should keep your DOM Bindings bound all of the time and have their execution dependent on the State of the view... sometimes the event handlers may do nothing, but that's fine. With this style of writing views, you're only concern with DOM events is that you remove your views properly. Inside the the view's state, you can consolidate the business logic behind when the views should respond to certain events.
What does State look like, in code?
initialize: function {
this.state = new Backbone.Model({ /* initial state */ });
}
Boom. It's that easy. State is just an object (or backbone model) where you can store data about the current state of the view. It's like the Views little junk drawer of useful data.
Should the save button be disabled?
this.state.set('save_button_disabled', true);
this.state.set('save_button_disabled', false);
Is the form validated? Errors?
this.state.set('form_valid', false);
this.state.set('form_errors', errorsArray);
Then bind some handlers to it, and when user does something, update the state and let the handlers handle it. Recording and responding to state changes will force you to write your views with lots of small functions that are easy to test, easy to name and easy to maintain. Having a dedicated object to store state is a great way to organize and consolidate the logic & conditions within the view.
this.listenTo(this.state, {
'change:save_button_disabled': this.toggleSaveButton,
'change:form_valid': this.onFormValidationChange
});
You can also tap into state within your views event handlers:
events: {
'click button.save': 'onSaveClicked'
},
onSaveClicked: function() {
if ( this.state.get('form_valid') ) {
/* do save logic */
}
}
As your application grows, you might also want to look into separating state into View State, Environment State (test, prod, dev, versions etc), User State (logged in/out, admin, permissions, users birthday? etc) and others. View State is usually the first step.
Your view should essentially be a bunch of small concise functions that respond to DOM, State and Model/Collection events. It should not contain the complex logic that evaluates, responds to and interprets user input and model data.. that complex stuff should exist in the Collections, Models and State. The view is simply a representation of those items and interface for the user to interact with them, just like the front-end of a web-app is an interface for the user to interact with a database.
View Code:
var MyView = Backbone.View.extend({
events: {
"click": "onClick"
},
"onClick": function() {
if ( this.state.get('clickable') ) {
/* do callback */
}
},
initialize: function(options) {
this.options = options;
this.state = new Backbone.Model({ clickable: true });
},
getTemplate: function() { /*...*/ },
render: function() {
var template = this.getTemplate(),
data = {
options: this.options,
data: this.model.toJSON(),
state: this.state.toJSON()
};
return this.$el.html(template(data));
}
});
Template Code:
{{#if state.logged_in}}
<p {{#if options.large}}class="font-big"{{/if}}>
Welcome back, {{data.userName}}.
</p>
{{else}}
<p>Hello! {{#sign_up_link}}</p>
{{/if}}
Here's a simple example: http://jsfiddle.net/CoryDanielson/o505ny1j/
More on state and this way of developing views
In a perfect world, a View is a merely a interactive representation of data and the many different states of an application. Views are state machines. The view is also a small buffer between the user and data. It should relay the user's intentions to models/collections as well as the model/collection responses back to the user.
More about this:
Model-View-Intent: http://futurice.com/blog/reactive-mvc-and-the-virtual-dom
Model-View-Intent slides: http://staltz.com/mvi-freaklies/#/
Also read the react.js docs to learn more about State as it applies to reactjs components which are an even lower-level representation of things than Backbone.Views. http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html
Let's say I'm trying to create a toDo application, where clicking each toDo opens an edit form for each toDoItem. I only want a maximum of one edit form open at any one time, so right now I am doing this in the edit method of the toDoItem view:
edit: function (e) {
e.preventDefault();
if ($('.editForm').length == 0) {
//create form model and view
}
}
That works, but doesn't seem very Backbone-y. Is there are way to select or count all instances of a particular view (in this case, the form-view)?
AFAIK, there are no utilities method in Backbone.View to count instances of a particular Views. Here are some ideas...
Maybe each of your TODO form is tied to a Model? In that case, you can have a model.set/get 'editing' and a collection.isAlreadyEditing() which would filter the models on this field:
(collection.filter(function(model){ return model.get("editing") }).length > 0
That would allow you to use on change:editing events throughout your views to control the logic and have convenient helpers functions in the collection to define some behavior of all those TODO as a whole. This would be one way to implement something closer to a Controller pattern in Backbone.
Another common thing in backbone is to keep an array of all the subviews when you instanciate them, so you could just do a:
_.any(subViews, function(view){return view.editing; })
Assuming that you keep a editing flag in your subviews when it gets toggled.
You can have your views listen to a toggleEdit event with the id or the model or something identifying what is being edited, sometimes the event handler can be as simple as a toggleClass("open", model==this.model)...
I am sure there are millions of other ideas. But counting jQuery selected elements is probably not very high on the list!
suppose there is a bookList View (with books as it's model) and some book view (with book as it's model)
If i want to run a function in every book view. there are a least two ways:
maintain an array which contain all book views in bookList view, than run the function directly
trigger a custom event on every book model in the books model to make the view run the function.
which one is better?
I really think you should consider a bit more things about your application and you will be guided to the decision by the natural flow of the concept of your application.
So according your methods :
Maintaining an array with views
If you don't have plenty of those bookView, it can be a solution. In fact, the question you should ask yourself here is do you really need this array only for that function, or could be used for something else.
Trigger a custom event on every book model
This will add to every bookView some identical event listeners and callbacks ( the function itself ). Which will require every bookView the memory share in your browser, for the same information.
I'm also thinking of a third method :
Trigger one custom event in your bookList View, which is listened by your Book views
So you could manage to do this and set an argument at the trigger with the function you want, so basically your code in your book View will be something like :
var cb = function(theFn) {
theFn.apply(this); // here we are setting theFn this to this view
}
bookList.on('doThisFunction', cb, this);
When you trigger you can do something like
theFn = function() {
this.render();
}
bookList.trigger('doThisFunction', theFn);
Be ware that when you no longer need the bookView you should turn off the eventlistener : bookList.off('doThisFunction', cb) from your book View , because you will have a garbage in your bookList event aggregator.
when my page opens, I call the collection and populate the view:
var pagColl = new pgCollection(e.models);
var pagView = new pgView({collection: pagColl});
Separately (via a Datepicker), I wish to want to populate the same collection with different models and instantiate the view again.
The problem I have is how to close the original pagView and empty the pagColl before I open the new one, as this "ghost view" is creating problems for me. The variables referred to above are local variables? is it that I need to create a global pagColl and reset() this?
well there has been many discussion on this topic actually,
backbone does nothing for you, you will have to do it yourself and this is what you have to take care of:
removing the view (this delegates to jQuery, and jquery removes it from the DOM)
// to be called from inside your view... otherwise its `view.remove();`
this.remove();
this removes the view from the DOM and removes all DOM events bound to it.
removing all backbone events
// to be called from inside the view... otherwise it's `view.unbind();`
this.unbind();
this removes all events bound to the view, if you have a certain event in your view (a button) which delegates to a function that calls this.trigger('myCustomEvent', params);
if you want some idea's on how to implement a system I suggest you read up on Derrick Bailey's blogpost on zombie views: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/.
another option
another option would be to reuse your current view, and have it re-render or append certain items in the view, bound to the collection's reset event
I was facing the same issue. I call the view.undelegateEvents() method.
Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM temporarily.
I use the stopListening method to solve the problem, usually I don't want to remove the entire view from the DOM.
view.stopListening();
Tell an object to stop listening to events. Either call stopListening
with no arguments to have the object remove all of its registered
callbacks ... or be more precise by telling it to remove just the
events it's listening to on a specific object, or a specific event, or
just a specific callback.
http://backbonejs.org/#Events-stopListening
Here's one alternative I would suggest to use, by using Pub/Sub pattern.
You can set up the events bound to the View, and choose a condition for such events.
For example, PubSub.subscribe("EVENT NAME", EVENT ACTIONS, CONDITION); in the condition function, you can check if the view is still in the DOM.
i.e.
var unsubscribe = function() {
return (this.$el.closest("body").length === 0);
};
PubSub.subscribe("addSomething",_.bind(this.addSomething, this), unsubscribe);
Then, you can invoke pub/sub via PubSub.pub("addSomething"); in other places and not to worry about duplicating actions.
Of course, there are trade-offs, but this way not seems to be that difficult.