Skip to content

Commit

Permalink
feat(Controllers): New SignalDomChildrenController which emits signal…
Browse files Browse the repository at this point in the history
…s when its children/descendants are added or removed
  • Loading branch information
Sub-Xaero committed Jan 28, 2022
1 parent 772ab33 commit 115ab44
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 0 deletions.
80 changes: 80 additions & 0 deletions docs/docs/controllers/signal/signal_dom_children_controller.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
id: SignalDOMChildrenController
title: SignalDOMChildrenController
---


import NoTargets from "../../_partials/no-targets.md";
import NoActions from "../../_partials/no-actions.md";
import NoSideEffects from "../../_partials/no-side-effects.md";
import NoEvents from "../../_partials/no-events.md";
import NoClasses from "../../_partials/no-classes.md";

## Purpose

A controller that listens for changes to the elements that are children/contents/descendents of the element that it is mounted on,
and emits a count of the number of elements that are in the child tree to any other Signal controllers that have subscribed by referencing
the same `nameValue` as this controller.

The emitted value can then be used in expressions in other `Signal*` controllers to enable more complex interactions.

A common use case for this would be to use this controller in conjunction with the `SignalVisibilityController`to show
a blank slate message like "You don't have any posts, create one now!" when there are no results on a page, and hide it again when items arrive.
Particularly in contexts where contents of a container can arrive and be removed remotely VIA Server-Sent-Events, AJAX,
TurboStreams, Websockets and even Third-Party JS interactions with the page.

Other use cases might be to show or hide elements when there is a specific number of items in the child tree, or greater than a certain number.


## [Actions](https://stimulus.hotwire.dev/reference/actions)
<NoActions/>

## [Targets](https://stimulus.hotwire.dev/reference/targets)
<NoTargets/>

## [Classes](https://stimulus.hotwire.dev/reference/classes)
<NoClasses/>

## [Values](https://stimulus.hotwire.dev/reference/values)

| Value | Type | Description | Default |
|----------------------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|
| `name` | String | The name/key to send signals using. This name should be the same as the `nameValue` of the other SignalControllers you want to sync with. | `-` |
| `scopeSelector` (Optional) | String | A CSS selector to pass to `querySelectorAll` to limit what elements are included in the count of empty/not-empty. You can use this to ignore certain elements from the count | All child elements of the controller root element |

## Events
<NoEvents/>

## Side Effects
<NoSideEffects/>

## How to Use

```html

<div data-controller="signal-dom-children" data-demo-target="output" data-signal-dom-children-name-value="notifications">
<!-- Notifications are put into this element by websockets, or a third-party library. -->
<!-- Notifications can be dismissed by clicking "X", which simply removes them from the DOM. -->
<!-- We don't need to call any methods, fire any events, or reference any controllers directly -->
<!-- The controller will emit a signal when elements are added or removed, and inform any listening controllers with the same `nameValue` of how many elements there are -->
</div>

<div
class="alert alert-success my-4"
data-controller="signal-visibility"
data-signal-visibility-name-value="notifications"
data-signal-visibility-show-value="<=0"
>
<!-- This message will only show when there are zero notifications -->
Well done! You've reached inbox zero.
Make yourself a cup of tea, you deserve it.
</div>

```

<iframe
src="https://stimulus-library.netlify.app/controllers/signal_dom_children_controller.html"
style={{width: "100%", height: "500px", border: "0", borderRadius: "4px", overflow: "hidden"}}
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
/>

55 changes: 55 additions & 0 deletions examples/controllers/signal_dom_children_controller.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link href="favicon.svg" rel="icon" type="image/svg+xml"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Signal DOM Children Controller</title>
</head>
<body>
<div class="container p-4 text-center">

<div data-controller="demo">
<div data-controller="signal-dom-children" data-demo-target="output" data-signal-dom-children-name-value="dom-visibility">
</div>

<a class="btn btn-primary" data-action="demo#addElement">Add Element</a>
<a class="btn btn-primary" data-action="demo#removeElement">Remove Element</a>
</div>

<div
class="alert alert-success my-4"
data-controller="signal-visibility"
data-signal-visibility-name-value="dom-visibility"
data-signal-visibility-show-value=">0"
>
There are elements in the observed container.
</div>

<div
class="alert alert-warning my-4"
data-controller="signal-visibility"
data-signal-visibility-name-value="dom-visibility"
data-signal-visibility-show-value="<=0"
>
No elements in the observed container.
Click a button to add one!
</div>

</div>

<style>
.hide {
display: none;
}

.favourite-colour {
padding: 2em;
margin: 2em;
border-radius: 5px;
border: 1px solid white;
}
</style>
<script src="/main.js" type="module"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions examples/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
ScrollToTopController,
SelfDestructController,
SignalActionController,
SignalDomChildrenController,
SignalInputController,
SignalVisibilityController,
StickyController,
Expand Down Expand Up @@ -145,6 +146,7 @@ app.register("remote-form", RemoteFormController);
app.register("responsive-iframe-body", ResponsiveIframeBodyController);
app.register("responsive-iframe-wrapper", ResponsiveIframeWrapperController);
app.register("signal-action", SignalActionController);
app.register("signal-dom-children", SignalDomChildrenController);
app.register("signal-input", SignalInputController);
app.register("signal-visibility", SignalVisibilityController);
app.register("scroll-container", ScrollContainerController);
Expand Down
1 change: 1 addition & 0 deletions src/controllers/signal/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './signal_action_controller';
export * from './signal_dom_children_controller';
export * from './signal_input_controller';
export * from './signal_visibility_controller';
51 changes: 51 additions & 0 deletions src/controllers/signal/signal_dom_children_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {BaseController} from "../../utilities/base_controller";
import {SignalPayload} from "./signal_input_controller";
import {EventBus} from "../../utilities";
import {signalConnectEvent, signalValueEvent} from "./events";
import {useMutationObserver} from "../../mixins/use_mutation_observer";
import {useEventBus} from "../../mixins/use_event_bus";

export class SignalDomChildrenController extends BaseController {

static values = {
name: String,
scopeSelector: String,
};
declare nameValue: string;
declare hasNameValue: boolean;
declare hasScopeSelectorValue: boolean;
declare scopeSelectorValue: string;

get _children(): Element[] {
if (this.hasScopeSelectorValue) {
return Array.from(this.el.querySelectorAll(this.scopeSelectorValue));
} else {
return Array.from(this.el.children);
}
}

get _name() {
if (this.hasNameValue) {
return this.nameValue;
} else {
throw new Error("SignalEmptyDomController requires a nameValue to be provided");
}
}

connect() {
useEventBus(this, signalConnectEvent(this._name), this.emitChildCount);
EventBus.emit(signalConnectEvent(this._name));
useMutationObserver(this, this.el, this.mutate, {childList: true});
this.emitChildCount();
}

mutate(entries: MutationRecord[]) {
this.emitChildCount();
}

emitChildCount() {
let childCount = this._children.length;
EventBus.emit(signalValueEvent(this._name), {element: this.el, value: childCount.toString()} as SignalPayload);
}

}

0 comments on commit 115ab44

Please sign in to comment.