How to limit event outcome to element of origin


#1

I’m attempting to use CanJS to:

  1. Create multiple instances of the same custom element (e.g., <canEl>)

  2. Act on a single instance of this custom element when that element is the origin of an event (for example a hover event).

For example, if I hover over a single <canEl> among many, I would like CanJS to modify content of that single element only.

I thought this would be natural and simple for CanJS, but despite much research and numerous attempts haven’t been able to cause CanJS to act on on the element of origin only. Instead, CanJS always seems to modify every instance of the custom element.

I realize I can work around this by simply not reusing the same custom element, or by adding a unique identifier to each instance of the same custom element. But I’m hoping for a more manageable and scalable solution.

I have also found some workarounds provided by CanJS such as custom-coding viewModel at a low level, or using enhancements to the standard framework (superMap?). But I would rather not push the limits of CanJS if possible.

Below is a JS Bin demonstrating my problem. Is there a simple, easy solution, and if so where might I have found it in the documentation or training? TIA.


Communication between "sibling" ViewModels
#2

Hi @code-read, there are a few ways you could do this!

What I would tend to do is make each of those canEl elements real components, so they maintain their own state. Here’s an example: https://jsbin.com/pukisaxucu/2/edit?html,output

To take that example a little further, what I would actually probably do in an app is have different messages show depending on the hover state… so instead of setting the message in the event listener, I would keep it in the template: https://jsbin.com/rewimajozu/2/edit?html,output

Or maybe it’d be better to keep the hovMessage as part of your ViewModel? Another alternative: https://jsbin.com/qahulobije/1/edit?html,output

If you had a list that you wanted to iterate over, then each item in the list could keep the state. This is covered a little bit by the can-component helpers docs.

Relying on the ViewModel to maintain your state is almost always what you want. But let’s imagine that we actually did need to manipulate the element that was hovered over… we could do something like this: https://jsbin.com/sutuyomura/1/edit?html,output

That last example shows that the events callbacks get called with 1) the component element and 2) the DOM event that was fired. You can always access the srcElement on the event to see what was hovered over, then do any DOM manipulation you want. Doing your own DOM manipulation kinda goes against the whole point of CanJS (write declarative templates!), but it’s a great escape hatch when you need it.

Hope that helps; I’m happy to explain any of the code above or help figure out good patterns for building components.


#3

Thank you @chasen, that’s an amazingly complete answer. Showing the alternatives and their relative merits in one place like this will help me choose wisely and consciously.

As you have shown, my fundamental misunderstanding is/was equating content in can.Component.extend()'s view: stanza with content configured directly in the HTML <body> element.

This experience has impressed upon me the importance of deciding whether to define elements in view: or <body>. However, I still need some guiding principles: Is this topic covered specifically in the documents or guides? If not I hope it will be.

(I had actually attempted several of the approaches you furnish, but because I was defining my target elements in view: none worked since they all shared the same root problem.)


P.S.: I tried your JS Bins, and all but one runs correctly under Firefox. The last one runs under Chrome but not Firefox (60.0 (64-bit); Windows 7/64 Pro). Here is a sample error:

TypeError: mouseoverEvent.srcElement is undefined
runner:36:17
error mouseover@https://null.jsbin.com/runner:36:17
controlMethod@https://unpkg.com/can@4/dist/global/can.js:5295:24
runner-4.1.4.min.js:1:9002

I hope this helps.


#4

Ha, that was a silly mistake. Firefox 62 will add support for srcElement; looks like every browser has support for event.target

the importance of deciding whether to define elements in view: or

It really just depends on what you want for your HTML/DOM structure. The examples above show multiple <can-el> components in the <body>, but you could choose to have one “root” component that then renders whatever you want, e.g. https://jsbin.com/toniwuhuwi/2/edit?html,output


#5

I think the tech overview guide touches on this when it gets to routing.


#6

@chasen, re:

I think I get it now: what is affected by setting a value in a ViewModel: field is constrained by the tag: field in their containing can.Component.extend() function.

So in this case, with tag: can-simple, I’m dealing with events and changing stache elements on all of a can-simple element’s contents regardless of <canEl> or other contained elements. For example,

<can-simple><canEl>{{hovMessage}}</canEl><canEl>{{hovMessage}}</canEl></can-simple>

is processed as a single element by can.Component.extend(), so setting hovMessage from within this function call changes both of the above <canEl> elements at once.

Whereas if I use tag: canEl instead, operations performed in the corresponding ViewModel: field are triggered by and affect a single canEl element regardless of any elements containing or contained by it.

Is that how you would explain it?

If so, I think this an important concept, especially when nesting view models as you did in your last example.

And (if I may expound a bit more), the above seems obvious and simple to me now but wasn’t obvious until I understood that CanJS can handle more than one view model per view. That’s a powerful concept to learn, and I think an example like this one might go a long way toward teaching it (IMHO :grinning:).


#7

In this example:

<can-simple><canEl>{{hovMessage}}</canEl><canEl>{{hovMessage}}</canEl></can-simple>

<canEl> is not a custom element, so it doesn’t have a view model and the browser isn’t doing anything special with it (because browsers just display the contents of elements they don’t understand).

If you had a <can-el> component (custom elements need a hyphen in their names), then it would have its own view model that it would render its view with. You can pass content to it (check here for an example).

The can-component docs have some good examples of multiple components in a single template: https://canjs.com/doc/can-component.html#Examples


#8

If you had a component (custom elements need a hyphen in their names), then it would have its own view model that it would render its view with.

Thanks, but I’m not sure that’s what I’m seeing. If I code:

tag: can-simple,
view: <can-el>{{hovMessage}}</can-el><can-el>{{hovMessage}}</can-el>,
ViewModel: {
        hovMessage: {
            default: "I was set via can-simple"
        },

Then {{hovMessage}} above gets set to I was set via can-simple even though the tag immediately surrounding it is can-el. If I code another ViewModel with the can-el tag, whichever comes first in the <script> tag determines the text displayed to the user. See https://jsbin.com/noqoduk/1/edit?html,output.


(Parenthetically, I’m finding that at least under Firefox, <canEl> does function as a custom element. I did some research and possibly the reason is that they inherit from HTMLUnknownElement. But I now understand that it’s not a good practice.)


#9

Hi @code-read, sorry it took so long to get a response.

For the first case (your JS Bin as-is), you’ve taught me something new about CanJS. :smiley: When no view is defined, it’ll use the component’s content as its view. Components without a view is such a rare thing (I’ve never seen that in an app) that I had no clue this feature existed. I’ve filed an issue for us to document it: https://github.com/canjs/can-component/issues/270

For the second case (when you flip the definitions so can-simple comes first), you’ll notice a warning in your console:

No custom element found for can-el

This is CanJS trying to warn you that it’s come across a tag for a custom element that hasn’t been registered. When the can-simple component gets defined, it looks for existing can-simple elements in the page and upgrades them to components; while it renders the component, can-el hasn’t been defined yet, so it warns you.

So, takeaways:

  • Make sure components are defined before they’re used in another stache template
  • Defining components without a view are not a common pattern in CanJS… I wouldn’t be surprised if there are weird bugs when doing this

#10

Thanks, the silence was getting a bit eerie :grinning:.

I discovered the console output you describe after I had posted my JSBin example. A clearer demonstration is to simply comment out my initial Component.extend() which has the same effect as switching it with the second one, w/o the console errors.* But I see you’ve gotten to the bottom of it regardless.

*(I tried to modify my JSBin to reflect that but for some reason I could not save my changes).