1.0.0 • Published 2 years ago

stimulus-invoke v1.0.0

Weekly downloads
-
License
MIT
Repository
gitlab
Last release
2 years ago

Stimulus Invoke

Fill the gap between Hotwire Turbo Frames/Streams responses and Stimulus controllers to replace Rails UJS.

It allows to invoke any Stimulus actions of any Stimulus controllers, anywhere in the DOM, automatically with the help of a Custom Element <stimulus-invoke>.

This package should be use in last resort. Basically when you need to interact with advanced/complex Stimulus controllers. Here some use cases:

  • Hide a modal when rendering a Turbo Stream response (where modal is a Stimulus controller and hide is one of its actions):
    <turbo-stream target="user_modal_1" action="append">
      <template>
        <stimulus-invoke action="modal#hide"></stimulus-invoke>
      </template>
    </turbo-stream>
  • Call a checkbox-list#checkboxAdded Stimulus action when a new checkbox (who is a target of the checkbox-list Stimulus controller) is added by a Turbo Stream response:

    <turbo-stream target="users_table_body" action="append">
      <template>
        <tr id="user_2">
          <td>
            <input type="checkbox" name="ids[]" value="2" data-checkbox-list-target="checkbox">
            User 2
          </td>
        </tr>
    
        <stimulus-invoke
          action="checkbox-list#checkboxAdded"
          source="#user_2 [data-checkbox-list-target~=checkbox]"
        ></stimulus-invoke>
      </template>
    </turbo-stream>

Behind the scene no magics 💫, it encapsulates a Stimulus controller who can interact with others Stimulus controllers.

Installation

npm

npm install stimulus-invoke

yarn

yarn add stimulus-invoke

Configuration

Using webpack

Stimulus Invoke integrates nicely with the webpack asset packager.

// src/application.js
import { Application } from '@hotwired/stimulus'
import { definitionsFromContext } from '@hotwired/stimulus-webpack-helpers'
import { StimulusInvokeController, StimulusInvokeElement } from 'stimulus-invoke'
window.Stimulus = Application.start()

// Load Stimulus controllers
const context = require.context('./controllers', true, /\.js$/)
Stimulus.load(definitionsFromContext(context))

// Register StimulusInvokeController
Stimulus.register('invoke', StimulusInvokeController)

// Register the <stimulus-invoke> custom element
StimulusInvokeElement.define()

Using Without a Build System

If you prefer not to use a build system, you can load Stimulus Invoke with UMD or ECMAScript module

With UMD

You can load Stimulus Invoke in a <script> tag and it will be globally available through the window.StimulusInvoke object:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.0.1/dist/stimulus.umd.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/stimulus-invoke/dist/stimulus-invoke.umd.min.js"></script>
  <script>
    (() => {
      const application = Stimulus.Application.start()

      application.register("hello", class extends Stimulus.Controller {
        static get targets() {
          return [ "name" ]
        }

        world() {
          this.nameTarget.value = 'hello#world method invoked!' // test
        }
      })

      // Register StimulusInvokeController
      application.register('invoke', StimulusInvoke.StimulusInvokeController)
    })()

    addEventListener('DOMContentLoaded', () => {
      // Register the <stimulus-invoke> custom element
      StimulusInvoke.StimulusInvokeElement.define()
    })
  </script>
</head>
<body>
  <div data-controller="hello">
    <input data-hello-target="name" type="text">

    <stimulus-invoke
      action="hello#world"
      source="[data-hello-target~=name]"
      keep
    ></stimulus-invoke>
  </div>
</body>
</html>

With ECMAScript module

You can load Stimulus Invoke in a <script type="module"> tag:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script type="module">
    import { Application, Controller } from 'https://cdn.skypack.dev/@hotwired/stimulus@3.0.1?min'
    import { StimulusInvokeController, StimulusInvokeElement } from 'https://cdn.skypack.dev/stimulus-invoke?min'

    window.Stimulus = Application.start()

    Stimulus.register('hello', class extends Controller {
      static targets = [ 'name' ]

      world() {
        this.nameTarget.value = 'hello#world method invoked!' // test
      }
    })

    // Register StimulusInvokeController
    Stimulus.register('invoke', StimulusInvokeController)

    addEventListener('DOMContentLoaded', () => {
      // Register the <stimulus-invoke> custom element
      StimulusInvokeElement.define()
    })
  </script>
</head>
<body>
  <div data-controller="hello">
    <input data-hello-target="name" type="text">

    <stimulus-invoke
      action="hello#world"
      source="[data-hello-target~=name]"
      keep
    ></stimulus-invoke>
  </div>
</body>
</html>

stimulus-invoke custom element

HTML attributes

AttributeDescriptionNotesPossible values
actionThe Stimulus action to invoke, similar to a Stimulus data-action attribute, but without the event partrequiredAt least one Stimulus action must be provide, but you can pass multiple actions separated by spaces (e.g. <stimulus-invoke action="class-attribute#toggle selectbox#open"></stimulus-invoke>)
targetThe DOM element(s) target. Should be an element connected to the Stimulus controller given in the action attribute or any of its sub elementsoptional By default it will find the closest element connected to the Stimulus controller given in the action attribute.It can be a DOM id (e.g. target="user_1") or a CSS selector who return all matches elements (e.g. <stimulus-invoke action="class-attribute#toggle" target=".user"></stimulus-invoke>)
sourceThe DOM element who dispatch the event (event.target and event.currentTarget) linked to the Stimulus action . Should be a sub element (i.e. a Stimulus target) of an element connected to the Stimulus controller given in the action attributeoptionalIt can be a DOM id (e.g. source="user_1") or a CSS selector who return only the first element (e.g. <stimulus-invoke action="class-attribute#toggle" source="[data-checkbox-list-target~=checkbox]"></stimulus-invoke>)
disabledPrevents any Stimulus action invocation when presentoptionale.g. <stimulus-invoke action="class-attribute#toggle" disabled></stimulus-invoke>
onEvent type who invoke the Stimulus action given in the action attributeoptional By default the action is invoked when the <stimulus-invoke> is connected to the DOM. When the on attribute is set the <stimulus-invoke> element is not automatically removed, but you can set the once attribute to automatically remove ite.g. <stimulus-invoke action="flash-message#close" on="click" target=".flash-message"><button>Close All</button></stimulus-invoke> You can pass multiple event types separated by spaces (e.g. <stimulus-invoke action="menu#toggle" on="mouseenter mouseleave" target="main_menu"><button>Help</button></stimulus-invoke>)
paramsPass parameters to the invoked Stimulus actionoptional Accept JSON Array encoded value only (i.e. JSON.stringify(array))e.g. <stimulus-invoke action="selectbox#search" params="[\"value\"]"></stimulus-invoke>
keepPreserve the <stimulus-invoke> element after invoking the Stimulus action given in the action attribute. Enabled by default if the on attribute is setoptional Opposite attribute of the once attribute, both attributes shouldn't be present at the same timee.g. <stimulus-invoke action="modal#hide" keep></stimulus-invoke>
onceAuto remove the <stimulus-invoke> element after invoking the Stimulus action given in the action attribute. Enabled by default if the on attribute is not setoptional Opposite attribute of the keep attribute, both attributes shouldn't be present at the same timee.g. <stimulus-invoke action="flash-message#close" on="click" target=".flash-message" once><button>Close All</button></stimulus-invoke>
data-controller-nameAdvanced usage. If you want to change the default encapsulate Stimulus controller nameoptional If not set, it'll use the StimulusInvokeElement.config.controllerName value, which is invoke by defaulte.g. <stimulus-invoke action="class-attribute#toggle" data-controller-name="custom-invoke"></stimulus-invoke>
data-controller-actionAdvanced usage. If you want to change the default encapsulate Stimulus action nameoptional If not set, it'll use the StimulusInvokeElement.config.controllerName value with the connected event and the StimulusInvokeElement.config.controllerName value with the apply action, which is invoke:connected->invoke#apply by defaulte.g. <stimulus-invoke action="class-attribute#toggle" data-controller-action="custom-event->invoke#customAction"></stimulus-invoke>

Custom element configuration

PropertyDescriptionNotesPossible values
controllerNameAdvanced usage. If you want to change the default linked Stimulus controller nameDefault value: invokee.g. StimulusInvokeElement.config.controllerName = 'invoker'
tagNameAdvanced usage. If you want to change the default <stimulus-invoke> tag nameDefault value: stimulus-invokee.g. StimulusInvokeElement.config.tagName = 'stimulus-invoker'
controllerTagNameAdvanced usage. If you want to change the default nested Stimulus controller tag nameDefault value: spane.g. StimulusInvokeElement.config.controllerTagName = 'div'

Use case with webpack

// src/application.js
import { Application } from '@hotwired/stimulus'
import { definitionsFromContext } from '@hotwired/stimulus-webpack-helpers'
import { StimulusInvokeController, StimulusInvokeElement } from 'stimulus-invoke'
window.Stimulus = Application.start()

// Load Stimulus controllers
const context = require.context('./controllers', true, /\.js$/)
Stimulus.load(definitionsFromContext(context))

// Register StimulusInvokeController
Stimulus.register('invoker', StimulusInvokeController)

// Config and register the <stimulus-invoker> custom element

// Overwrite the default linked Stimulus controller name, 'invoke' by default
StimulusInvokeElement.config.controllerName = 'invoker'

// Overwrite the default <stimulus-invoke> tag name, 'stimulus-invoke' by default
StimulusInvokeElement.config.tagName = 'stimulus-invoker'

// Overwrite the default nested Stimulus controller tag name, 'span' by default
StimulusInvokeElement.config.controllerTagName = 'div'

// Register the <stimulus-invoker> custom element
StimulusInvokeElement.define()
<!-- src/index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="/src/application.js" defer></script>
</head>
<body>
  <div data-controller="hello">
    <input data-hello-target="name" type="text">

    <stimulus-invoker
      action="hello#world"
      source="[data-hello-target~=name]"
    ></stimulus-invoker>
  </div>
</body>
</html>

Properties

IMPORTANT: To access properties from a <stimulus-invoke> element, you need to use the StimulusInvokeElementReactive class instead of the StimulusInvokeElement class. In your main javascript file (e.g. src/application.js), you can replace:

//...
import { StimulusInvokeController, StimulusInvokeElement } from 'stimulus-invoke'
//...
StimulusInvokeElement.define()
//...

By:

//...
import { StimulusInvokeController, StimulusInvokeElementReactive } from 'stimulus-invoke'
//...
StimulusInvokeElementReactive.define()
//...
AttributeTypeNotesPossible values
actionstringrequirede.g. myStimulusInvokeElement.action = 'class-attribute#toggle'
targetstringoptionale.g. myStimulusInvokeElement.target = '.user'
sourcestringoptionale.g. myStimulusInvokeElement.source = 'user_1'
onstringoptionale.g. myStimulusInvokeElement.on = 'click'
paramsstringoptionale.g. myStimulusInvokeElement.params = "[\"value\"]"
disabledbooleanoptionale.g. myStimulusInvokeElement.disabled = true
keepbooleanoptionale.g. myStimulusInvokeElement.keep = true
oncebooleanoptionale.g. myStimulusInvokeElement.once = true

Methods

IMPORTANT: To access methods from a <stimulus-invoke> element, you need to use the StimulusInvokeElementReactive class instead of the StimulusInvokeElement class, see the Properties section for more info.

AttributeDescriptionPossible values
apply()Advanced usage. Manually invoke the Stimulus action given in the action attributee.g. myStimulusInvokeElement.apply()
destroy()Advanced usage. Destroy the nested invoke Stimulus controller and the <stimulus-invoke> custom elemente.g. myStimulusInvokeElement.destroy()

StimulusInvoke controller

Controller configuration

PropertyDescriptionNotesPossible values
domAccessAdvanced usage. Allow to invoke any DOM Element methods, ⚠️ use at your own risk ⚠️Type: boolean Default value: falsee.g. StimulusInvokeController.config.domAccess = true
domIdentifierVery advanced usage. Useful only if domAccess is true and you already have a real Stimulus controller named DOM. Special Stimulus controller identifier to use DOM elements as Stimulus controllersType: string Default value: DOMe.g. StimulusInvokeController.config.domIdentifier = 'native-element'
propertyWritableAdvanced usage. Allow to set value for any Stimulus Controller or DOM Element properties, ⚠️ use at your own risk ⚠️Type: boolean Default value: falsee.g. StimulusInvokeController.config.propertyWritable = true

Use case with webpack

// src/application.js
import { Application } from '@hotwired/stimulus'
import { definitionsFromContext } from '@hotwired/stimulus-webpack-helpers'
import { StimulusInvokeController, StimulusInvokeElement } from 'stimulus-invoke'
window.Stimulus = Application.start()

// Load Stimulus controllers
const context = require.context('./controllers', true, /\.js$/)
Stimulus.load(definitionsFromContext(context))

// Config and register StimulusInvokeController

// Allow to invoke any DOM Element methods,
// use at your own risk, false by default
StimulusInvokeController.config.domAccess = true

// Allow to set value for any Stimulus Controller or DOM Element properties,
// use at your own risk, false by default
StimulusInvokeController.config.propertyWritable = true

// Register StimulusInvokeController
Stimulus.register('invoke', StimulusInvokeController)

// Register the <stimulus-invoke> custom element
StimulusInvokeElement.define()
<!-- src/index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="/src/application.js" defer></script>
</head>
<body>
  <div>
    <details id="menu">
      <summary>Menu</summary>
      Something small enough to escape casual notice.
    </details>

    <stimulus-invoke
      on="click"
      action="DOM#click"
      target="#menu > summary"
    >
      <button>Toggle menu</button>
    </stimulus-invoke>
  </div>
</body>
</html>

Running examples

You can test Stimulus Invoke locally before adding this library to one of your projects.

To test Stimulus Invoke examples, you need to install Node.js on your computer, then follow these instructions:

  1. Clone this repository:
git clone https://gitlab.com/efficiently/stimulus-invoke
cd stimulus-invoke/
  1. Once in your local stimulus-invoke directory, run:
npm install
npm run fresh-build
  1. Launch the local web server:
npm start
  1. Then you can open in your Web browser this URL: http://localhost:8000

  2. For each examples, you should read the source of the web page to better understand what's going on. Or open the corresponding HTML file, here.

Running tests

You can run Stimulus Invoke tests locally.

To run tests, you need to install Node.js on your computer, then follow these instructions:

  1. Clone this repository:
git clone https://gitlab.com/efficiently/stimulus-invoke
cd stimulus-invoke/
  1. Once in your local stimulus-invoke directory, run:
npm install
npm run fresh-build
  1. Run tests:
npm test

Changelog

See CHANGELOG.md file for reference.

Getting Help & Contributing Back

Have a question about Stimulus Invoke? Connect with other Stimulus developers on the Hotwire Discourse community forum.

Find a bug? Head over to our issue tracker and we'll do our best to help. We love pull/merge requests, too!

We expect all Stimulus Invoke contributors to abide by the terms of our Code of Conduct.

Acknowledgments

Stimulus Invoke is MIT-licensed open-source software.

1.0.0

2 years ago

1.0.0-beta.3

2 years ago

1.1.0-alpha

2 years ago

1.0.0-beta.2

3 years ago

1.0.0-beta.1

3 years ago

1.0.0-alpha.1

3 years ago

1.0.0-alpha

3 years ago

0.1.0

3 years ago

0.1.0-rc.1

3 years ago

0.1.0-beta.10

3 years ago

0.1.0-beta.9

3 years ago

0.1.0-beta.8

3 years ago

0.1.0-beta.7

3 years ago

0.1.0-beta.6

3 years ago

0.1.0-beta.5

3 years ago

0.1.0-beta.4

3 years ago

0.1.0-beta.3

3 years ago

0.1.0-beta.2

3 years ago

0.1.0-beta

3 years ago

0.1.0-alpha.6

3 years ago

0.1.0-alpha.5

3 years ago

0.1.0-alpha.4

3 years ago

0.1.0-alpha.2

3 years ago

0.1.0-alpha.1

3 years ago