How do I determine the scope of a Stimulus controller? - javascript

Say, we have a Stimulus controller that toggles a sidebar. The button that triggers the toggle action is located in different places, though, depending on the device. E.g. in the header when you are on a mobile device and in the main navigation when you are on a Desktop device.
What do you do in this situation? Is it better to initialise 2 Stimulus controllers (one in the div that belongs to the header and one that belongs to the main navigation) or to initialise just one Stimulus controller, for example in a wrapper div tag that encloses both the header as well as the main navigation?

I would put a sidebar controller on the body tag.
As long as the sidebar controller is not reused it's totally fine. It wouldn't work for a generic toggler controller.
<body data-controller="sidebar">
<header class="sm:hidden">
<button data-action="sidebar#toggle">Toggle sidebar</button>
</header>
<section data-sidebar-target="sidebar">
Some content in the sidebar
</section>
<main>
<button class="hidden sm:inline-block" data-action="sidebar#toggle">Toggle sidebar</button>
</main>
</body>

I would recommend avoiding controllers with too much scope where practical. Having a large section of the DOM with multiple controllers on it could risk some performance issues.
An alternative approach to having a sidebar & sidebar-toggle controller is to have a generic 'fire an event' controller.
We can even take inspiration from Alpine.js and its x-on directive. We could have a smaller scoped 'on' action do something type controller that let's us have a generic way to trigger any event.
HTML
Let's start with the HTML, similar to the previous answer, we have a sidebar div and then two buttons that do basically the same thing (trigger an event).
We can leverage Stimulus' approach to coordinating with DOM events to help us here but this time with a custom global event with the name 'sidebar:toggle'.
Note that the sidebar controller listens to the event with #window to ensure that it picks up any 'bubbling' events all the way up the DOM.
https://stimulus.hotwired.dev/reference/controllers#cross-controller-coordination-with-events
<body>
<header class="sm:hidden">
<button
data-controller="on"
data-action="click->on#go"
data-on-event-name-param="sidebar:toggle"
>
Toggle sidebar
</button>
</header>
<section data-controller="sidebar" data-action="sidebar-toggle#window->sidebar#layout">
Some content in the sidebar
</section>
<main>
<button
class="hidden sm:inline-block"
data-controller="on"
data-action="click->on#go"
data-on-event-name-param="sidebar:toggle"
>
Toggle sidebar
</button>
</main>
</body>
JavaScript (Controllers)
The Sidebar controller will have whatever you want but also a method for toggle that we can trigger with the action.
import { Controller } from '#hotwired/stimulus';
class Sidebar extends Controller {
static targets = [];
static values = { expanded: { default: false, type: Boolean } };
connect() {
// do the things
}
toggle() {
this.expandedValue = !this.expandedValue;
}
}
The On controller leverages Stimulus action parameters to be able to receive any dynamic value set on the DOM as it's event.
Note that the On controller intentionally makes the events bubble (this is the default, but good to be explicit) so that any other event listeners on window can listen to this.
import { Controller } from '#hotwired/stimulus';
class On extends Controller {
go({ params: { eventName } }) {
this.dispatch(eventName, { prefix: '', bubbles: true, cancelable: false });
}
}

Related

How to reset scroll on route changes in ember 3?

I'm trying to use hook activate, didTransition, or willTransition, but none of these work, they do nothing at all.
I try to start by one route:
// app/routes/section.js
import Route from '#ember/routing/route';
export default class SectionRoute extends Route {
...
activate() { scroll(0, 0); }
}
<!-- app/templates/section.hbs -->
<h1>{{model.title}}</h1>
<p>{{model.body}}</p>
{{outlet}}
{{#each model.subsections as |s| }}
<Section #section={{s}} />
{{/each}}
This works in my js browser console:
scroll(0, 0);
This is my router
// app/router.js
...
Router.map(function() {
this.route('docs');
this.route('section', { path: '/docs/section/:slug' });
});
If I make section a child of docs, it works, as long as I don't hide parent's content, but I want to hide it.
An anchor in link-to component could help.
When I remove this piece of css, it works.
html {
scroll-behavior: smooth;
}
Your approach is working as expected. I have created an Ember Twiddle to verify that one, which you can find here. It has two routes. One which scrolls to the top when activated and one which scrolls to bottom when activated. It's scrolling as expected.
I guess you may be facing issues with the hook if transitioning between subroutes? A route is considered to stay active and the activate hook is not called
if a transition does not change the route but only the dynamic segments or query parameters used for the route or
if a transition happens between subroutes of that route.
If you want to have scroll to top on every transition or want to have more granular control, which transitions should trigger that scrolling I would recommend to use routeDidChange event of RouterService instead.

Angular show custom content (arrows) on overflow after content comes remotely

I have a div which has data coming in remotely from a subscribed service
overflow-y is set to scroll
I have to put custom arrows when the overflow happens and user can click, and I have a scroll method with a #templateRef which will handle manual scrolling
template and ts look like below
#ViewChild('scrollContent') scrollContent: ElementRef
ptivate data
private overflow: boolean = false
constructor (private service: DataService, private cdr: ChangeDetectorRef) {}
ngOnInit () {
this.service.subscribe(data => {
this.data = data
this.isOverflow()
})
isOverFlow () {
if (this.scrollContent.nativeElement.scrollHeight > this.scrollContent.nativeElement.clientHeight) {
this.overflow = true
} else {
this.overflow = false
}
}
}
<div *ngIf="overflow" (click)="scroll($event,'up')">
<div class="uparrow">
</div>
</div>
<div class="content" #scrollContent>
<!--
Iterate or display data thus changing the content of div and might cause
overflow-y
-->
</div>
<div *ngIf="overflow" (click)="scroll($event,'down')">
<div class="downarrow">
</div>
</div>
But isOverflow() returns false and console.log runs before and prints false before the view is updated
I tried some changeDetectionMethods like cdr.detectChanges() and markForChange() did not have any luck with it.
I can manually trigger a setTimeOut and that might work but I am looking for angular specific answer
P.S. I have tried some life-cycle-hooks too but obviously you don't want them to trigger like 1000 times a second so I have avoided using some of them unless I am missing one which solves this purpose and gets called exactly once for example ngAfterViewChecked() is not performant at all as it keeps getting called again and again
Angular version is 5.2.10
Thanks

Responding to a click on a Vue element programmatically

Essentially, I want my Vue instance to respond to a click on an uploaded thumbnail.
I'm using the FineUploader Vue package with the template layout per the docs (see end of the question). Upon uploading an image, a tree like this is outputted:
<Root>
<Gallery>
<Thumbnail>
</Gallery>
</Root>
Coming from a jQuery background I really have no idea about the 'correct' way to go about this given that the Thumbnail Template is defined by the package already, and so I'm not creating my own Thumbnail template. I know that I can access elements like this:
let thumb = this.$el.querySelector('.vue-fine-uploader-thumbnail');
And perhaps a listener
thumb.addEventListener('click', function() {
alert('I got clicked');
});
But dealing with the Vue instance being re-rendered etc. I'm not familiar with.
Vue Template:
<template>
<Gallery :uploader="uploader" />
</template>
<script>
import FineUploaderTraditional from 'fine-uploader-wrappers'
import Gallery from 'vue-fineuploader/gallery'
export default {
components: {
Gallery
},
data () {
const uploader = new FineUploaderTraditional({
options: {/*snip*/}
})
return {
uploader
}
}
}
</script>
In order to respond to click events you add the v-on:click (or it's short form: #click) to whatever tag you want.
If you have elements that are nested that respond to the click event you might experience that a click on a child triggers a parents click event. To prevent this you add #click.stop instead, so that it doesn't trigger the parents click.
So you would have something along the lines of:
<Gallery #click="myFunction" />

Scroll to a certain element of the current page in Angular 4

Upon clicking a button (which is bottom of the page), I want to go to a certain element (in my case, #navbar) which is in the top of the current page, but I don't know how to do it. I've tried the following code with no avail.
<nav class="navbar navbar-light bg-faded" id="navbar">
<a class="navbar-brand" href="#">
{{appTitle}}
</a>
<!-- rest of the nav link -->
</nav>
<!-- rest of the page content -->
<!-- bottom of the page -->
<button class="btn btn-outline-secondary" (click)="gotoTop()">Top</button>
In angular component:
import { Router } from '#angular/router';
/* rest of the import statements */
export class MyComponent {
/* rest of the component code */
gotoTop(){
this.router.navigate([], { fragment: 'navbar' });
}
}
I would really appreciate if someone helped me out with a solution and explained why my code hadn't worked.
Please note that element (navbar) is in other component.
You can do this with javascript:
gotoTop() {
let el = document.getElementById('navbar');
el.scrollTop = el.scrollHeight;
}
This will bring the DOM element with id="navbar" into view when the method is called. There's also the option of using Element.scrollIntoView. This can provide a smooth animation and looks nice, but isn't supported on older browsers.
If the element is in a different component you can reference it several different ways as seen in this question.
The easiest method for your case would likely be:
import { ElementRef } from '#angular/core'; // at the top of component.ts
constructor(myElement: ElementRef) { ... } // in your export class MyComponent block
and finally
gotoTop() {
let el = this.myElement.nativeElement.querySelector('nav');
el.scrollIntoView();
}
Working plunker.
I know, you want to scroll to a specific element in the page. But, if the element is in the top of the page, then you can use the following:
window.scrollTo(0, 0);
document.getElementById("YOUR_DIV_ID").scrollIntoView({ behavior: 'smooth' });
I think your way didn't work because of the empty router.
this.router.navigate(['/home'], { fragment: 'top' });
would work if 'home' is declared as a route and you have the id="top" element on it.
I know you would like it to be "pure Angular way", but this should work (at least):
gotoTop(){
location.hash = "#navbar";
}

Simple Javascript not working in my spa app

I am using the Hot Towel template by John Papa. I have a html view called nav.html, which contains the header portion of my spa. Within that, i need to display the name of the person that is logged into the system (i have a server side utility class that handles the query).
The following is from the html in the nav.html view for that-
data-bind="text: LoggedInAs"
Here is the viewmodel code (nav.js)-
define(['services/logger'], function (logger) {
var vm = {
activate: activate,
title: 'Nav View'
};
return vm;
//#region Internal Methods
function activate() {
logger.log('Nav View Activated', null, 'Nav', true);
return true;
}
//#endregion
});
My problem is that i am not sure how to do this. i tried adding nav.js to my viewmodels folder, but the javascript does not run. I thought durandal would have picked it up like the other viewmodels. The only difference between the nav.js and the other view models is that the other view models are triggered by clicking on a link (wired through route.mapnav).
What am i missing here? How do i get the javascript to run without a user clicking on a link? When the page loads, I need nav.js to run in order to populate the LoggedInAs data-bind.
Make sure that you are activating your nav view. In the example code you have given in the comment above, it would need to be this:
<header> <!--ko compose: {view: 'nav', activate: true} --><!--/ko--> </header>

Categories

Resources