# Sticky user card contents theme

**URL:** https://meta.discourse.org/t/sticky-user-card-contents-theme/159875
**Category:** Development
**Created:** [August 5, 2020, 9:13am UTC](https://meta.discourse.org/t/sticky-user-card-contents-theme/159875 "2020-08-05T09:13:23Z")
**Posts on this page:** 6
**Page:** 1

<div class="post-metadata">

### Author: ![sonicb](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sonicb/32/186395_2.png) [@sonicb](https://meta.discourse.org/u/sonicb)
#### Post date: [August 5, 2020, 9:13am UTC](https://meta.discourse.org/t/sticky-user-card-contents-theme/159875/1 "2020-08-05T09:13:23Z")

</div>

I’ve created a more elaborate user-card-contents theme, adding some custom user fields. I’d like to make the card sticky, with a close button - in a similar manner to the “Create a new Topic” component.

Am I right in thinking that I will need to overwrite the user-card-contents.js to prevent the call to close the element? Would I be able to package this within the theme?

Thanks!

---

<div class="post-metadata">

### Author: ![Johani](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/johani/32/176920_2.png) [@Johani](https://meta.discourse.org/u/Johani)
#### Post date: [August 5, 2020, 4:24pm UTC](https://meta.discourse.org/t/sticky-user-card-contents-theme/159875/2 "2020-08-05T16:24:56Z")

</div>

Hey Pete. Welcome to Meta 👋

> [@sonicb](#):
>
> Would I be able to package this within the theme?

the short answer is yes. If your changes only affect the front-end, then they can be done in a theme / component.

Can you please elaborate on what you mean by sticky? If you mean that you want it to scroll with the content while remaining in the same position then that would be a CSS change. Also, is the change intended to be included on both desktop and mobile?

> [@sonicb](#):
>
> Am I right in thinking that I will need to overwrite the user-card-contents.js to prevent the call to close the element?

Can you please describe which call you’re are referring to specifically? (as on on click or on scroll etc)

> [@sonicb](#):
>
> with a close button

The markup for the close button would need to be added to the handlebars template for the user card. The logic to handle the action when the button is clicked would need be added to the component `.js` file.

> [@sonicb](#):
>
> need to overwrite the user-card-contents.js

A full override might not be necessary, there are some hooks that you can use to override specific methods in a class. I can share more about that if you describe what you want to do a bit more.

---

<div class="post-metadata">

### Author: ![sonicb](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sonicb/32/186395_2.png) [@sonicb](https://meta.discourse.org/u/sonicb)
#### Post date: [August 6, 2020, 9:26am UTC](https://meta.discourse.org/t/sticky-user-card-contents-theme/159875/3 "2020-08-06T09:26:33Z")

</div>

Thanks for the welcome Joe!

> [@Johani](#):
>
> Can you please elaborate on what you mean by sticky? If you mean that you want it to scroll with the content while remaining in the same position then that would be a CSS change. Also, is the change intended to be included on both desktop and mobile?

What I’m looking to do is prevent the `card-contents-base.js` mixin event handler `clickOutsideEventName` from closing the card on desktop. Instead I would like to force users to click a button to close it. I’ll likely need to do something different for mobile.

> [@Johani](#):
>
> The markup for the close button would need to be added to the handlebars template for the user card. The logic to handle the action when the button is clicked would need be added to the component `.js` file.

Got this handlebars template working, now onto figuring out the .js 🙂

---

<div class="post-metadata">

### Author: ![Johani](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/johani/32/176920_2.png) [@Johani](https://meta.discourse.org/u/Johani)
#### Post date: [August 7, 2020, 11:57am UTC](https://meta.discourse.org/t/sticky-user-card-contents-theme/159875/4 "2020-08-07T11:57:06Z")

</div>

TL;DR I think this is what you’re after

Theme JS

```javascript
api.modifyClass("component:user-card-contents", {
  didInsertElement() {
    this._super(...arguments);
    $("html").off(this.clickOutsideEventName);
  },
  actions: {
    closeCard() {
      this._close();
    }
  }
});

```

and then add this to your template somewhere

```plaintext
{{d-button
  class="btn-flat"
  action=(action "closeCard")
  icon="times"
}}

```

**The long version**

You probably already know most of this since you’ve already worked on your theme but I will try to keep it a little bit more detailed for the broader audience.

> [@sonicb](#):
>
> clickOutsideEventName

The first thing I’d do is to search either locally or on Github. On Github you’d get something like [this](https://github.com/discourse/discourse/search?q=clickOutsideEventName&unscoped_q=clickOutsideEventName). In most cases, a search term will have more than one result and you’d either have to be more specific or manually scan the result to find something close to what you want.

So, now we end up on this file

[discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js)

This file is a [Mixin](https://api.emberjs.com/ember/release/classes/Mixin/methods). Why do I mention this? Because you need to be aware that mixins can be shared in a number of different places. In this case, it’s used for both user cards and group cards. So, the changes you make here will affect both.

If you search in the file, you’ll find that `clickOutsideEventName` is first defined here

[discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L83)

then passed on here as a property

[discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L90)

and then finally consumed here

[discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L97-L114)

Cool, but what does all of that mean? Well if you look at where all of this code is added, you’ll notice that it’s inside `didInsertElement`

[discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L78-L139)

The [Ember guides](https://guides.emberjs.com/v3.4.0/components/the-component-lifecycle/#toc_formatting-component-attributes-with-didreceiveattrs) state

> Ember guarantees that, by the time `didInsertElement()` is called:
> 
> 1. The component’s element has been both created and inserted into the DOM.
> 2. The component’s element is accessible via the component’s `this.element` property.

Why do we need this? Because we need a different mousedown handler for user cards and group cards. If we go back a bit, you’ll now notice that the id of the element is used in the `clickOutsideEventName`

> <https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L81-L83>

Which like we discussed above is then passed on as a property.

Now, let’s move on to how all of this relates to what you’re doing.

You’re trying to prevent the cards from being closed when the user clicks outside of them. So, let’s try to find out a way to do that. If you recall, we discussed how `clickOutsideEventName` eventually gets consumed here

> <https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L97-L114>

In a nutshell this adds a mousedown handler to the HTML element when a card is inserted (on the first page view). We then check the target of the mousedown event. If the target is somewhere in the card, we bail. If it’s outside of the card, we close it by calling `this._close()`

`_close()` is defined here

[discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L269-L286)

And this is what you’ll need to call when you add your close button - but we’ll get back to that later.

Now, the goal is to remove this mousedown handler if you want clicks outside the card not to close it. So how do we do this? Well, we’ll need to modify `didInsertElement()` and here’s how that can be done.

Discourse themes have access to the [plugin API](https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/app/lib/plugin-api.js), which contains many methods that you can use. There’s a bit more details about the most commonly used ones [here](https://meta.discourse.org/t/developer-s-guide-to-discourse-themes/93648#heading--4-c)

If you remember, the file we’re working with is a Mixin. A Mixin is an Ember class. So, the method we’re going to use is `modifyClass`

[https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/app/lib/plugin-api.js#L121-L124](https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/app/lib/plugin-api.js#L121-L124)

When you use `modifyClass` you can either add, modify or completely override a class method. We’ll focus on modifying a method since that’s what you want to do.

We want to modify `didInsertElement()` so we can do something like this

```javascript
api.modifyClass('mixin:card-contents-base', {
  didInsertElement() {
    console.log("foo");
  }
});

```

and try it out…

well that didn’t work.

 ![modify Mixin class](https://global.discourse-cdn.com/meta/original/3X/5/6/56665d89853d0ccf6d1a7287e8366448872bfbad.png)

why is that? Well it turns out that the `modifyClass` method doesn’t currently support modifying Mixins (as far as I’ve tested) I will make a note to figure out why that’s the case and check if we can fix that. For now though, let’s get back to what you want to do.

Well, we can’t modify Mixins, so I guess we’re stuck, yeah? No. Let’s dig a little bit deeper.

Like I mentioned before, that Mixin is used by both the user [user-card-contents](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/components/user-card-contents.js) and [group-card-contents](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/components/group-card-contents.js) Ember [components](https://api.emberjs.com/ember/release/classes/Component) (again, because Mixins are designed to make code reusable)

So, let’s look at the `user-card-contents` component here

[discourse/app/assets/javascripts/discourse/app/components/user-card-contents.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/components/user-card-contents.js)

If you read carefully, you’ll notice that we first import the Mixin we discussed above [here](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/components/user-card-contents.js#L11)

and then create a new Ember component and pass the Mixin to it.

> <https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/components/user-card-contents.js#L16>

What does that mean? it means that `didInsertElement()` for `user-card-contents` is actually inherited from the Mixin. The same can be said about `group-card-contents`.

Where does that leave us? Well, there’s good news and bad news. The good news is that if you want to make changes to the `user-card-contents` without affecting `group-card-contents` then you can! The bad news is that if you want your changes to apply to both, then you’ll have to duplicate some code.

Let’s go back to `modifyClass` and try again with `user-card-contents`. So something like this:

```javascript
api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    console.log("foo");
  }
});

```

and see what happens…

 ![modify user card contents](https://global.discourse-cdn.com/meta/original/3X/d/9/d94c886acb6c245404cdaa18ae98f7fbcab69df4.png)

voalá 🎉

The change is registered and we can see it in the console, but we’re not quite there yet.

So, now that we can modify `didInsertElement()`, let’s try to get back to what you’re trying to do. If you remember, the mousedown handler is defined in `didInsertElement` here

[discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L97-L114)

So what can we do about it? Well, it’s a simple as this

`$("html").off(clickOutsideEventName)`

Will that work though? Nope. If you do this

```javascript
api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    $("html").off(clickOutsideEventName)
  }
});

```

You’ll end up with something broken. Why is that? because that’s not a modification of the method. That’s a full override of the core `didInsertElement()` method for that component. So, none of the code in core is actually applied if you do this.

How do we fix this? Well it turns out that Ember has a thing called `this._super(...arguments)`

What does that do? it allows you to append or prepend code in addition to what the class method already has. For example, if you do this

```javascript
api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // code you want to add
    $("html").off(clickOutsideEventName);
    // code from core
    this._super(...arguments);
  }
});

```

then the code you want to add will run before anything else here

[discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L79-L138)

However, if you do this…

```javascript
api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // code from core
    this._super(...arguments);
    // code you want to add
    $("html").off(clickOutsideEventName);
  }
});

```

then your code will run after everything here runs

[discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L79-L138)

This is a great way to keep your theme resilient to changes in core since everything in core runs first, then your code runs after that. So, let’s try this again and see what happens.

```javascript
api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // code from core
    this._super(...arguments);
    // code you want to add
    $("html").off(clickOutsideEventName);
  }
});

```

and…

 ![error modifying method](https://global.discourse-cdn.com/meta/original/3X/b/8/b8581304cbb66ec083283c33297b64a7e0256802.png)

Why is this happening? it’s because of different code context. In the Ember component file `clickOutsideEventName` is already defined by the time it’s consumed. Your theme is in a different file, so `clickOutsideEventName` is not defined there.

How do we fix it? Remember this?

> <https://github.com/discourse/discourse/blob/e5dc843185feb268c277bb0ee4db9666d6452783/app/assets/javascripts/discourse/app/mixins/card-contents-base.js#L89-L95>

`clickOutsideEventName` is a property of the component, so if you use `this.clickOutsideEventName` then it should work. Let’s try that.

```javascript
api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // code from core
    this._super(...arguments);
    // code you want to add
    $("html").off(this.clickOutsideEventName);
  },
});

```

And indeed it works 🎉

The user cards now open without any errors and clicking anywhere outside doesn’t do anything.

You can do the exact same thing for group cards like so

```javascript
api.modifyClass('component:group-card-contents', {
  didInsertElement() {
    // code from core
    this._super(...arguments);
    // code you want to add
    $("html").off(this.clickOutsideEventName);
  },
});

```

The only thing left is to hookup that close button to the `_close()` method we discussed earlier and there are three steps to this.

1. add a `closeCard` action (you can name whatever you want)
2. add a button to the user card template
3. call that action when the button is clicked.

I’ll stick with the user cards for simplicity. So, we add this (along with what we already discussed)

```javascript
api.modifyClass("component:user-card-contents", {
  didInsertElement() {
    this._super(...arguments);
    $("html").off(this.clickOutsideEventName);
  },
  // new stuff 
  actions: {
    closeCard() {
      this._close();
    }
  }
});

```

all that does is that it calls the core `_close()` method whenever the `closeCard` custom action is triggered.

Next we need to add a button to the user card template or something like this

```plaintext
{{d-button
  class="btn-flat"
  action=(action "closeCard")
  icon="times"
}}

```

My rough results look like this

And of course you can do something similar for group cards like I mentioned above.

I’ll leave the group card and mobile implementation as exercise for you since you’d essentially use the exact same concepts we discussed above if you want to make changes to those but please let me know if you run into any problems.

---

<div class="post-metadata">

### Author: ![sonicb](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sonicb/32/186395_2.png) [@sonicb](https://meta.discourse.org/u/sonicb)
#### Post date: [August 13, 2020, 1:00pm UTC](https://meta.discourse.org/t/sticky-user-card-contents-theme/159875/6 "2020-08-13T13:00:58Z")

</div>

Thanks so much, this made for a great tutorial! I really appreciate it. I hadn’t realised about the Plugin API.

One thing that wasn’t immediately obvious above was to wrap the theme JS changes in script tags and place it into the plugin’s `common/head_tag.html`:

```
<script type="text/discourse-plugin" version="0.2">
</script>

```

Just out of curiosity does the version number in the tag matter here? And is it always best to place these in the `head_tag.html` over the `header.html` or does it not really matter.

Thanks!

---

<div class="post-metadata">

### Author: ![sonicb](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sonicb/32/186395_2.png) [@sonicb](https://meta.discourse.org/u/sonicb)
#### Post date: [August 13, 2020, 1:21pm UTC](https://meta.discourse.org/t/sticky-user-card-contents-theme/159875/7 "2020-08-13T13:21:30Z")

</div>

@Johani I can create another thread for this if you’d prefer, but I’m trying to track down the client-side `user-card` controller which:

- loads the user-card on initial click
- and redirects to the user-summary on second click

My aim is to remove the second-click functionality. Thanks!
