Communication between "sibling" ViewModels

I’m attempting to use CanJS to build a web page where changes to one section produce changes to another section. I want to treat each section as a separate ViewModel so that I can use plain JavaScript to configure elements (using loops to build a series of elements, controlled via getPropertyValue() to match CSS grid size).

What I keep running into is, any element “listening” CanJS provides seems to rely on both elements being under the same “parent context.” Otherwise, a change in one ViewModel does not trigger a change in the other. This forces me to code all interactions within a single ViewModel, which in turn seems to reduce my ability to “program” element content via JavaScript.*

Am I correct? I will provide full source code if requested; to keep this question simple, I’ve reduced it to a basic example here (see embedded comments): https://jsbin.com/livuvax/edit?html,output. Any help appreciated, thanks.


*In hopes of keeping my code clear, have resisted contrivances such as constructing a DOM element to pass values through.

In hopes of keeping my code clear, have resisted contrivances such as constructing a DOM element to pass values through

I might be interpreting this comment incorrectly, but this is exactly what I would encourage. If one component has state that should affect another component, you kind of have two options:

  1. Create a parent component where that state can be routed.
  2. Use some other mechanism for maintaining that state, e.g. a module that both components import.

I would almost always go for #1 unless it’s some global state that needs to be used at different hierarchies throughout the application (like info about the logged in user).

For #1, you can use a binding to get some data out of one component and into another: JS Bin - Collaborative JavaScript Debugging

[Note that I split up some of the properties; you could bind them directly without the clickData middleman, but as soon as the components are instantiated, the properties will be synced. What you’d probably want is something closer to what’s in the demo, because then the click state is kept separately from the default values that should be shown in each component.]

Here’s another example where the parent doesn’t have to have the properties defined because scope.vars is used: JS Bin - Collaborative JavaScript Debugging

For #2, you might have an object that has the user’s info or other state. If you’re using a module loader/bundler, I’d keep that in a separate file and then create one instance when it’s exported, but for this JS Bin it’s just a global variable: JS Bin - Collaborative JavaScript Debugging

2 Likes

@chasen, thanks for this thorough reply. Having verified that #2 works, I am now attempting to go for #1 for simplicity’s sake.

Things were going well until I tried to use a {{stache}} expression to configure a property of a tag (as opposed to its “innerHTML”). For some reason, the “child” ViewModel works for innerHTML and the “parent” ViewModel for the tag property (“style={{stache}}”).

If I could use a “child” ViewModel to configure styles or classes, I think it would help me control individual child elements more cleanly because events that fire on the parent ViewModel alter all instances of a {{stache}} at once.

Does that make sense?

I’ve composed a JSBin to illustrate; see the embedded comment for detail. Again, any help appreciated:

That JS Bin has a can-el component without a view, which is not a common pattern in CanJS apps. It’s not documented anywhere; I opened an issue about it: https://github.com/canjs/can-component/issues/270

That said, you can pull a value out of a child view model with :to, as shown in this example: https://jsbin.com/yolasejohe/1/edit?html,output

In most cases, you’d want to have some CSS to style the component vs. putting the CSS in JS. https://jsbin.com/favejumacu/1/edit?html,output

Hi @code-read I’m a fan of this solution in donejs, not sure exactly how this is made in canjs but there must be a way to do it.

This is my proposed solution in donejs: Having a shared instance object

It is based on the $root, $parent and $parents[depth] keywords/properties that knockoutjs (very similar to angular) uses: http://knockoutjs.com/documentation/binding-context.html

Thanks, and I apologize: in trying to clarify my question, I oversimplified my illustration and skewed your answer to it. The JSBin I furnished contains only one <can-el> element, but the model I’m attempting to construct needs 16 such elements, each with slightly different attributes (innerHTML and background-color: it’s a color-picker).

I’m hoping to listen for a click event on each <can-el> in order to use the whole set of <can-el>s as a 16-position “switch” to control other parts of the web app. And I want to use stache to change a clicked element’s style and distinguish it visually.

Currently I define the set of 16 <can-el>s as a single string with a JavaScript for() loop, then use that string as my (parent) Component.extend()'s view: definition. This works fine, but because I’m coding it at the parent level, any event: defined in this Component.extend() affects all <can-el>s at once (the problem you helped me avoid in How to limit event outcome to element of origin).

This problem of “overgeneralization” led me to define a separate Component.extend() specifically for <can-el>, but without a view:. This approach almost works: its ViewModel traps clicks on individual <can-el> elements and configures stache content in their specific innerHTMLs, but does not configure their class (or other “outerHTML” content) via stache (as demonstrated in https://jsbin.com/levunun/edit?html,output).

Conceptually, I’m attempting to 1) build a series of similar HTML elements in a loop; and 2) listen and respond to changes and/or events occurring on each one separately. I’m looking for the simplest and most “ideomatic” way of doing so via CanJS.