phlex-stimulus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +16 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +341 -0
  6. data/Rakefile +12 -0
  7. data/lib/generators/phlex/stimulus/controller/USAGE +5 -0
  8. data/lib/generators/phlex/stimulus/controller/controller_generator.rb +49 -0
  9. data/lib/generators/phlex/stimulus/controller/templates/component.rb.erb +30 -0
  10. data/lib/generators/phlex/stimulus/controller/templates/controller.ts.erb +23 -0
  11. data/lib/generators/phlex/stimulus/generator_mixin.rb +21 -0
  12. data/lib/generators/phlex/stimulus/install/USAGE +5 -0
  13. data/lib/generators/phlex/stimulus/install/install_generator.rb +111 -0
  14. data/lib/generators/phlex/stimulus/install/templates/app/components/base.rb.erb +29 -0
  15. data/lib/generators/phlex/stimulus/install/templates/app/components/chain_controller.rb.erb +29 -0
  16. data/lib/generators/phlex/stimulus/install/templates/app/components/classlist_controller.rb.erb +30 -0
  17. data/lib/generators/phlex/stimulus/install/templates/app/components/controller.rb.erb +10 -0
  18. data/lib/generators/phlex/stimulus/install/templates/app/components/controller_chain.rb.erb +32 -0
  19. data/lib/generators/phlex/stimulus/install/templates/app/components/controller_template.rb.erb +24 -0
  20. data/lib/generators/phlex/stimulus/install/templates/app/components/log_controller.rb.erb +33 -0
  21. data/lib/generators/phlex/stimulus/install/templates/app/components/sleep_controller.rb.erb +30 -0
  22. data/lib/generators/phlex/stimulus/install/templates/app/components.rb.erb +7 -0
  23. data/lib/generators/phlex/stimulus/install/templates/app/javascript/application.ts.erb +5 -0
  24. data/lib/generators/phlex/stimulus/install/templates/app/javascript/controllers/application.ts.erb +10 -0
  25. data/lib/generators/phlex/stimulus/install/templates/app/javascript/controllers/chain_controller.ts.erb +32 -0
  26. data/lib/generators/phlex/stimulus/install/templates/app/javascript/controllers/chainable_controller.ts.erb +44 -0
  27. data/lib/generators/phlex/stimulus/install/templates/app/javascript/controllers/classlist_controller.ts.erb +30 -0
  28. data/lib/generators/phlex/stimulus/install/templates/app/javascript/controllers/index.ts.erb +4 -0
  29. data/lib/generators/phlex/stimulus/install/templates/app/javascript/controllers/log_controller.ts.erb +25 -0
  30. data/lib/generators/phlex/stimulus/install/templates/app/javascript/controllers/sleep_controller.ts.erb +19 -0
  31. data/lib/generators/phlex/stimulus/install/templates/app/javascript/controllers/typed_controller.ts.erb +53 -0
  32. data/lib/generators/phlex/stimulus/install/templates/app/javascript/utils/misc.ts.erb +37 -0
  33. data/lib/generators/phlex/stimulus/install/templates/app/javascript/utils/template.ts.erb +11 -0
  34. data/lib/phlex/stimulus/components/base.rb +81 -0
  35. data/lib/phlex/stimulus/components/controller.rb +170 -0
  36. data/lib/phlex/stimulus/components.rb +9 -0
  37. data/lib/phlex/stimulus/version.rb +8 -0
  38. data/lib/phlex/stimulus.rb +15 -0
  39. data/sorbet/config +7 -0
  40. data/sorbet/rbi/annotations/.gitattributes +1 -0
  41. data/sorbet/rbi/annotations/actionmailer.rbi +15 -0
  42. data/sorbet/rbi/annotations/actionpack.rbi +430 -0
  43. data/sorbet/rbi/annotations/actionview.rbi +75 -0
  44. data/sorbet/rbi/annotations/activejob.rbi +50 -0
  45. data/sorbet/rbi/annotations/activemodel.rbi +119 -0
  46. data/sorbet/rbi/annotations/activerecord.rbi +172 -0
  47. data/sorbet/rbi/annotations/activesupport.rbi +561 -0
  48. data/sorbet/rbi/annotations/globalid.rbi +30 -0
  49. data/sorbet/rbi/annotations/minitest.rbi +116 -0
  50. data/sorbet/rbi/annotations/railties.rbi +104 -0
  51. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  52. data/sorbet/rbi/dsl/.gitattributes +1 -0
  53. data/sorbet/rbi/dsl/active_support/callbacks.rbi +21 -0
  54. data/sorbet/rbi/gems/.gitattributes +1 -0
  55. data/sorbet/rbi/gems/action_text-trix@2.1.19.rbi +9 -0
  56. data/sorbet/rbi/gems/actioncable@8.1.3.rbi +9 -0
  57. data/sorbet/rbi/gems/actionmailbox@8.1.3.rbi +9 -0
  58. data/sorbet/rbi/gems/actionmailer@8.1.3.rbi +9 -0
  59. data/sorbet/rbi/gems/actionpack@8.1.3.rbi +20478 -0
  60. data/sorbet/rbi/gems/actiontext@8.1.3.rbi +9 -0
  61. data/sorbet/rbi/gems/actionview@8.1.3.rbi +14722 -0
  62. data/sorbet/rbi/gems/activejob@8.1.3.rbi +9 -0
  63. data/sorbet/rbi/gems/activemodel@8.1.3.rbi +9 -0
  64. data/sorbet/rbi/gems/activerecord@8.1.3.rbi +9 -0
  65. data/sorbet/rbi/gems/activestorage@8.1.3.rbi +9 -0
  66. data/sorbet/rbi/gems/activesupport@8.1.3.rbi +20153 -0
  67. data/sorbet/rbi/gems/ast@2.4.3.rbi +550 -0
  68. data/sorbet/rbi/gems/base64@0.3.0.rbi +545 -0
  69. data/sorbet/rbi/gems/benchmark@0.5.0.rbi +621 -0
  70. data/sorbet/rbi/gems/booleans@0.1.3.rbi +31 -0
  71. data/sorbet/rbi/gems/builder@3.3.0.rbi +9 -0
  72. data/sorbet/rbi/gems/byebug@12.0.0.rbi +49 -0
  73. data/sorbet/rbi/gems/concurrent-ruby@1.3.8.rbi +10656 -0
  74. data/sorbet/rbi/gems/connection_pool@3.0.2.rbi +9 -0
  75. data/sorbet/rbi/gems/crass@1.0.7.rbi +648 -0
  76. data/sorbet/rbi/gems/date@3.5.1.rbi +391 -0
  77. data/sorbet/rbi/gems/drb@2.2.3.rbi +9 -0
  78. data/sorbet/rbi/gems/erb@6.0.6.rbi +1600 -0
  79. data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
  80. data/sorbet/rbi/gems/globalid@1.4.0.rbi +9 -0
  81. data/sorbet/rbi/gems/i18n@1.15.2.rbi +2285 -0
  82. data/sorbet/rbi/gems/io-console@0.8.2.rbi +9 -0
  83. data/sorbet/rbi/gems/json@2.21.1.rbi +2289 -0
  84. data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +189 -0
  85. data/sorbet/rbi/gems/logger@1.7.0.rbi +896 -0
  86. data/sorbet/rbi/gems/loofah@2.25.2.rbi +1134 -0
  87. data/sorbet/rbi/gems/mail@2.9.1.rbi +9 -0
  88. data/sorbet/rbi/gems/marcel@1.2.1.rbi +9 -0
  89. data/sorbet/rbi/gems/mini_mime@1.1.5.rbi +9 -0
  90. data/sorbet/rbi/gems/minitest@6.0.6.rbi +1482 -0
  91. data/sorbet/rbi/gems/net-imap@0.6.4.1.rbi +9 -0
  92. data/sorbet/rbi/gems/net-pop@0.1.2.rbi +9 -0
  93. data/sorbet/rbi/gems/net-protocol@0.2.2.rbi +244 -0
  94. data/sorbet/rbi/gems/net-smtp@0.5.1.rbi +9 -0
  95. data/sorbet/rbi/gems/netrc@0.11.0.rbi +147 -0
  96. data/sorbet/rbi/gems/nio4r@2.7.5.rbi +9 -0
  97. data/sorbet/rbi/gems/nokogiri@1.19.4.rbi +8168 -0
  98. data/sorbet/rbi/gems/parallel@1.28.0.rbi +270 -0
  99. data/sorbet/rbi/gems/phlex-rails@2.4.0.rbi +1945 -0
  100. data/sorbet/rbi/gems/phlex@2.4.1.rbi +1413 -0
  101. data/sorbet/rbi/gems/pp@0.6.4.rbi +387 -0
  102. data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +455 -0
  103. data/sorbet/rbi/gems/prism@1.9.0.rbi +42226 -0
  104. data/sorbet/rbi/gems/racc@1.8.1.rbi +165 -0
  105. data/sorbet/rbi/gems/rack-session@2.1.2.rbi +756 -0
  106. data/sorbet/rbi/gems/rack-test@2.2.0.rbi +681 -0
  107. data/sorbet/rbi/gems/rack@3.2.6.rbi +4653 -0
  108. data/sorbet/rbi/gems/rackup@2.3.1.rbi +9 -0
  109. data/sorbet/rbi/gems/rails-dom-testing@2.3.0.rbi +517 -0
  110. data/sorbet/rbi/gems/rails-html-sanitizer@1.7.1.rbi +620 -0
  111. data/sorbet/rbi/gems/rails@8.1.3.rbi +9 -0
  112. data/sorbet/rbi/gems/railties@8.1.3.rbi +3507 -0
  113. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +362 -0
  114. data/sorbet/rbi/gems/rake@13.4.2.rbi +3130 -0
  115. data/sorbet/rbi/gems/rbi@0.4.0.rbi +5706 -0
  116. data/sorbet/rbi/gems/rbs@4.0.3.rbi +6908 -0
  117. data/sorbet/rbi/gems/refract@1.1.0.rbi +9 -0
  118. data/sorbet/rbi/gems/reline@0.6.3.rbi +2446 -0
  119. data/sorbet/rbi/gems/require-hooks@0.4.0.rbi +152 -0
  120. data/sorbet/rbi/gems/rexml@3.4.4.rbi +4905 -0
  121. data/sorbet/rbi/gems/rubocop-espago@1.3.0.rbi +9 -0
  122. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +988 -0
  123. data/sorbet/rbi/gems/rubydex@0.2.9.rbi +841 -0
  124. data/sorbet/rbi/gems/securerandom@0.4.1.rbi +75 -0
  125. data/sorbet/rbi/gems/shoulda-context@2.0.0.rbi +544 -0
  126. data/sorbet/rbi/gems/spoom@1.8.4.rbi +6738 -0
  127. data/sorbet/rbi/gems/tapioca@0.19.2.rbi +3599 -0
  128. data/sorbet/rbi/gems/thor@1.5.0.rbi +3870 -0
  129. data/sorbet/rbi/gems/timeout@0.6.1.rbi +200 -0
  130. data/sorbet/rbi/gems/tsort@0.2.0.rbi +389 -0
  131. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +5474 -0
  132. data/sorbet/rbi/gems/unicode-display_width@3.2.0.rbi +130 -0
  133. data/sorbet/rbi/gems/unicode-emoji@4.2.0.rbi +332 -0
  134. data/sorbet/rbi/gems/uri@1.1.1.rbi +2402 -0
  135. data/sorbet/rbi/gems/useragent@0.16.11.rbi +9 -0
  136. data/sorbet/rbi/gems/websocket-driver@0.8.2.rbi +9 -0
  137. data/sorbet/rbi/gems/websocket-extensions@0.1.5.rbi +9 -0
  138. data/sorbet/rbi/gems/zeitwerk@2.8.2.rbi +9 -0
  139. data/sorbet/rbi/shims/gems/parser.rbi +12 -0
  140. data/sorbet/rbi/shims/gems/rails.rbi +55 -0
  141. data/sorbet/rbi/shims/gems/refract.rbi +13 -0
  142. data/sorbet/rbi/shims/gems/shoulda-context.rbi +16 -0
  143. data/sorbet/tapioca/compilers/phlex_controller.rb +80 -0
  144. data/sorbet/tapioca/config.yml +26 -0
  145. data/sorbet/tapioca/extensions/load_gem.rb +1 -0
  146. data/sorbet/tapioca/require.rb +4 -0
  147. metadata +266 -0
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Components
5
+ # Takes the names of controllers that will get connected
6
+ # in a particular order.
7
+ class ChainController < Controller
8
+ self.controller_name = 'chain'
9
+
10
+ #: (chain: String) -> void
11
+ def initialize(chain:)
12
+ @chain = chain
13
+ end
14
+
15
+ # @override
16
+ #: ?{ -> void } -> void
17
+ def view_template(&block)
18
+ div(
19
+ data: {
20
+ controller: self.class.controller_name,
21
+ controller_chain: @chain,
22
+ },
23
+ ) do
24
+ block&.call
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Components
5
+ # Adds or removes classes in HTML elements.
6
+ class ClasslistController < Controller
7
+ self.controller_name = 'classlist'
8
+
9
+ actions :add, :remove
10
+ targets :subject
11
+
12
+ #: (classlist: Array[String?]) -> void
13
+ def initialize(classlist:)
14
+ @classlist = classlist
15
+ end
16
+
17
+ # @override
18
+ #: ?{ -> void } -> void
19
+ def view_template(&block)
20
+ div(
21
+ data: {
22
+ controller: self.class.controller_name,
23
+ class_list: class_list!(@classlist),
24
+ },
25
+ ) do
26
+ block&.call
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ # Abstract class for all Phlex components
5
+ # that wrap Stimulus.js controllers in this app.
6
+ #
7
+ # @abstract
8
+ class Components::Controller < Phlex::Stimulus::Components::Controller
9
+ # define any custom helpers for controller components here
10
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Components
5
+ # Takes instances of stimulus controller components
6
+ # and renders them in such a way that they
7
+ # will get attached in the given order.
8
+ #
9
+ # It will wait until the `connect` logic of one controller
10
+ # finishes before it attempts to connect another controller.
11
+ class ControllerChain < Base
12
+ #: (*Controller?) -> void
13
+ def initialize(*controllers)
14
+ @controllers = controllers.compact #: Array[Controller]
15
+ end
16
+
17
+ #: -> void
18
+ def view_template
19
+ # optimisation when only one controller is present
20
+ return render @controllers.first if @controllers.length == 1
21
+
22
+ render ChainController.new(
23
+ chain: strlist!(@controllers.map(&:controller_name)),
24
+ ) do
25
+ @controllers.each.with_index do |controller, i|
26
+ render ControllerTemplate.new(controller: controller, index: i)
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Components
5
+ # Renders a template with a deferred Stimulus controller inside.
6
+ #
7
+ # Useful with `ChainController` to create a chain of controllers
8
+ # that will get attached in a particular order one after another.
9
+ class ControllerTemplate < Base
10
+ #: (controller: Controller, index: Integer) -> void
11
+ def initialize(controller:, index:)
12
+ @controller = controller
13
+ @index = index
14
+ end
15
+
16
+ #: -> void
17
+ def view_template
18
+ template(class: "#{@controller.controller_name}-template-#{@index}") do
19
+ render @controller
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Components
5
+ # Performs a `console.log` after connecting.
6
+ # Mainly used in development and for debugging.
7
+ class LogController < Controller
8
+ self.controller_name = 'log'
9
+
10
+ actions :log
11
+
12
+ #: (message: String, ?on_event: bool) -> void
13
+ def initialize(message:, on_event: false)
14
+ @message = message
15
+ @on_event = on_event
16
+ end
17
+
18
+ # @override
19
+ #: ?{ -> void } -> void
20
+ def view_template(&block)
21
+ div(
22
+ data: {
23
+ controller: self.class.controller_name,
24
+ message: @message,
25
+ on_event: attrbool(@on_event),
26
+ },
27
+ ) do
28
+ block&.call
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Components
5
+ # Sleeps for the given amount of milliseconds.
6
+ # Useful in controller sequences to delay the execution
7
+ # of `connect` logic of some other controller.
8
+ class SleepController < Controller
9
+ self.controller_name = 'sleep'
10
+
11
+ #: (timeout: Integer) -> void
12
+ def initialize(timeout:)
13
+ @timeout = timeout
14
+ end
15
+
16
+ # @override
17
+ #: ?{ -> void } -> void
18
+ def view_template(&block)
19
+ div(
20
+ data: {
21
+ controller: self.class.controller_name,
22
+ timeout: @timeout.to_s,
23
+ },
24
+ ) do
25
+ block&.call
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ # Contains all components of the app
5
+ module Components
6
+ extend Phlex::Kit
7
+ end
@@ -0,0 +1,5 @@
1
+ // Entry point for the build script in your package.json
2
+
3
+ // uncomment if you use turbo
4
+ // import "@hotwired/turbo-rails"
5
+ import "./controllers"
@@ -0,0 +1,10 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+
3
+ const application = Application.start()
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false;
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ (window as any).Stimulus = application
9
+
10
+ export { application }
@@ -0,0 +1,32 @@
1
+ import { addEventPromise } from "../utils/misc"
2
+ import { application } from "./application"
3
+ import { Controller } from "@hotwired/stimulus"
4
+
5
+ /**
6
+ * Takes the names of controllers that will be connected
7
+ * in a particular order.
8
+ * It waits for the logic of `connect` to finish before
9
+ * connecting another controller.
10
+ **/
11
+ class ChainController extends Controller<HTMLElement> {
12
+ controllerChain!: string
13
+
14
+ async connect() {
15
+ let dataset = this.element.dataset
16
+ this.controllerChain = dataset.controllerChain!
17
+
18
+ let i = 0
19
+ for (let controllerName of this.controllerChain.split(' ')) {
20
+ let controllerTemplate = this.element.querySelector(`template.${controllerName}-template-${i}`) as HTMLTemplateElement
21
+ let controllerElement = document.importNode(controllerTemplate.content, true)
22
+ let readyPromise = addEventPromise(this.element, `${controllerName}:ready`)
23
+
24
+ this.element.appendChild(controllerElement)
25
+ await readyPromise
26
+
27
+ i++
28
+ }
29
+ }
30
+ }
31
+
32
+ application.register("chain", ChainController)
@@ -0,0 +1,44 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { MakeControllerWithTargetsFactory } from "./typed_controller"
3
+
4
+ /**
5
+ * An abstract class for Stimulus controllers that can be chained
6
+ * using the `ChainController`.
7
+ */
8
+ export abstract class ChainableController<ElementType extends Element> extends Controller<ElementType> {
9
+ async connect() {
10
+ await this.init()
11
+ this.dispatch("ready")
12
+ }
13
+
14
+ /**
15
+ * The `connect` logic in a chainable controller
16
+ * should be defined under `init`
17
+ */
18
+ abstract init(): Promise<void>
19
+ }
20
+
21
+ /**
22
+ * An abstract class for Stimulus controllers that can be chained
23
+ * using the `ChainController` and supports fully typed targets.
24
+ *
25
+ * In order to use it specify the `Element` type and
26
+ * a record of target definitions.
27
+ *
28
+ * ```
29
+ * class MyController extends TypedController<
30
+ * HTMLFormElement,
31
+ * {
32
+ * input: HTMLInputElement,
33
+ * submitButton: HTMLElement,
34
+ * }
35
+ * >() {
36
+ * static targets = [ "input", "submitButton" ]
37
+ *
38
+ * connect() {
39
+ * console.log("Hello!")
40
+ * }
41
+ * }
42
+ * ```
43
+ **/
44
+ export const TypedChainableController = MakeControllerWithTargetsFactory(ChainableController)
@@ -0,0 +1,30 @@
1
+ import { application } from "./application"
2
+ import { TypedController } from "./typed_controller"
3
+
4
+ /**
5
+ * Adds or removes classes in HTML elements.
6
+ **/
7
+ class ClasslistController extends TypedController<HTMLElement, { subject: HTMLDivElement }>() {
8
+ static targets = [ "subject" ]
9
+
10
+ classList!: string[]
11
+
12
+ connect() {
13
+ let dataset = this.element.dataset
14
+ this.classList = dataset.classList!.split(" ")
15
+ }
16
+
17
+ add() {
18
+ for (let subject of this.subjectTargets) {
19
+ subject.classList.add(...this.classList)
20
+ }
21
+ }
22
+
23
+ remove() {
24
+ for (let subject of this.subjectTargets) {
25
+ subject.classList.remove(...this.classList)
26
+ }
27
+ }
28
+ }
29
+
30
+ application.register("classlist", ClasslistController)
@@ -0,0 +1,4 @@
1
+ // import all your controllers here
2
+ import "./log_controller"
3
+ import "./sleep_controller"
4
+ import "./classlist_controller"
@@ -0,0 +1,25 @@
1
+ import { application } from "./application"
2
+ import { ChainableController } from "./chainable_controller"
3
+
4
+ /**
5
+ * Performs a `console.log` after connecting.
6
+ * Mainly used in development and for debugging.
7
+ **/
8
+ class LogController extends ChainableController<HTMLElement> {
9
+ message!: string
10
+
11
+ async init() {
12
+ let dataset = this.element.dataset
13
+ this.message = dataset.message!
14
+ let onEvent = dataset.onEvent
15
+ if (onEvent) return
16
+
17
+ this.log()
18
+ }
19
+
20
+ log() {
21
+ console.log(this.message)
22
+ }
23
+ }
24
+
25
+ application.register("log", LogController)
@@ -0,0 +1,19 @@
1
+ import { sleep } from "../utils/misc"
2
+ import { application } from "./application"
3
+ import { ChainableController } from "./chainable_controller"
4
+
5
+ /**
6
+ * Sleeps for the given amount of milliseconds.
7
+ * Useful in controller sequences to delay the execution
8
+ * of `connect` logic of some other controller.
9
+ **/
10
+ class SleepController extends ChainableController<HTMLElement> {
11
+ async init() {
12
+ let dataset = this.element.dataset
13
+ let timeout = Number(dataset.timeout)
14
+
15
+ await sleep(timeout)
16
+ }
17
+ }
18
+
19
+ application.register("sleep", SleepController)
@@ -0,0 +1,53 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* eslint-disable @typescript-eslint/no-empty-object-type */
3
+ import { Controller } from "@hotwired/stimulus"
4
+
5
+ type TargetDefinitions = Record<string, HTMLElement>
6
+
7
+ type StimulusTargetProps<T extends TargetDefinitions> = {
8
+ [K in keyof T as `${K & string}Target`]: T[K]
9
+ } & {
10
+ [K in keyof T as `${K & string}Targets`]: T[K][]
11
+ } & {
12
+ [K in keyof T as `has${Capitalize<K & string>}Target`]: boolean
13
+ }
14
+
15
+ type AbstractStimulusControllerConstructor = abstract new (...args: any[]) => Controller<any>
16
+
17
+ export function MakeControllerWithTargetsFactory<TBase extends AbstractStimulusControllerConstructor>(Base: TBase) {
18
+ return function <
19
+ TElement extends Element,
20
+ TTargets extends TargetDefinitions = {},
21
+ >() {
22
+ type Props = StimulusTargetProps<TTargets>
23
+
24
+ return Base as unknown as abstract new (
25
+ ...args: ConstructorParameters<TBase>
26
+ ) => Controller<TElement> & Props
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Main factory function for creating base classes
32
+ * for typed stimulus controllers.
33
+ *
34
+ * In order to use it specify the `Element` type and
35
+ * a record of target definitions.
36
+ *
37
+ * ```
38
+ * class MyController extends TypedController<
39
+ * HTMLFormElement,
40
+ * {
41
+ * input: HTMLInputElement,
42
+ * submitButton: HTMLElement,
43
+ * }
44
+ * >() {
45
+ * static targets = [ "input", "submitButton" ]
46
+ *
47
+ * connect() {
48
+ * console.log("Hello!")
49
+ * }
50
+ * }
51
+ * ```
52
+ */
53
+ export const TypedController = MakeControllerWithTargetsFactory(Controller)
@@ -0,0 +1,37 @@
1
+ // throws an error when the value is null or undefined
2
+ // otherwise returns the value
3
+ export function must<T>(value: T): NonNullable<T> {
4
+ if (value === undefined || value === null) {
5
+ throw new Error('value must be defined')
6
+ }
7
+
8
+ return value
9
+ }
10
+
11
+ /**
12
+ * Convert a boolean HTML Element attribute to a JS boolean.
13
+ */
14
+ export function attrbool(value: unknown): boolean {
15
+ return value === 'true'
16
+ }
17
+
18
+ export function sleep(timeout: number | undefined): Promise<void> {
19
+ if (timeout === undefined) return Promise.resolve()
20
+
21
+ return new Promise((resolve) => {
22
+ setTimeout(() => {
23
+ resolve()
24
+ }, timeout)
25
+ })
26
+ }
27
+
28
+ /**
29
+ * Registers an event listener on the given target
30
+ * and returns a promise that gets resolved when
31
+ * the event was caught by the listener.
32
+ */
33
+ export function addEventPromise(target: EventTarget, type: string): Promise<Event> {
34
+ return new Promise((resolve) => {
35
+ target.addEventListener(type, (event) => resolve(event))
36
+ })
37
+ }
@@ -0,0 +1,11 @@
1
+ export function cloneTemplate(id: string): DocumentFragment {
2
+ let errTemplate = document.querySelector(`template#${id}`) as HTMLTemplateElement
3
+ return document.importNode(errTemplate.content, true)
4
+ }
5
+
6
+ export function cloneTemplateInto(templateId: string, targetContainerId: string): DocumentFragment {
7
+ let container = document.getElementById(targetContainerId)!
8
+ let newNode = cloneTemplate(templateId)
9
+ container.appendChild(newNode)
10
+ return newNode
11
+ }
@@ -0,0 +1,81 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Phlex::Stimulus
5
+ module Components
6
+ # Abstract class for Phlex components that render HTML with better sorbet and stimulus support.
7
+ #
8
+ # @abstract
9
+ class Base < Phlex::HTML
10
+ #: -> ApplicationController::HelperProxy
11
+ def h = view_context
12
+
13
+ #: -> String
14
+ def inspect
15
+ "#<#{self.class.name}>"
16
+ end
17
+
18
+ # Constructs a Stimulus event hook string
19
+ # eg.
20
+ #
21
+ # event('click', 'summary#redirect') #=> "click->summary#redirect"
22
+ #
23
+ #: (String, String) -> String
24
+ def event(event_name, full_action_name)
25
+ "#{event_name}->#{full_action_name}"
26
+ end
27
+
28
+ # Converts a Ruby boolean to a boolean
29
+ # value in HTML attributes.
30
+ #
31
+ # Returns `"true"` for a truthy value, otherwise `nil`.
32
+ #
33
+ #: (bool) -> String?
34
+ def attrbool(val)
35
+ 'true' if val
36
+ end
37
+
38
+ # Output a single newline character.
39
+ # If a block is given, a space will be output before and after the block.
40
+ #
41
+ #: ?{ -> void } -> void
42
+ def newline(&block)
43
+ plain "\n"
44
+ return unless block
45
+
46
+ block.call
47
+ plain "\n"
48
+ end
49
+
50
+ # Merges the given list of class names (with optional nils)
51
+ # into a single class string separated by spaces.
52
+ #
53
+ #: (*String?) -> String
54
+ def class_list(*args)
55
+ buff = String.new
56
+
57
+ args.each do |arg|
58
+ next unless arg
59
+
60
+ buff << arg << ' '
61
+ end
62
+
63
+ buff.strip
64
+ end
65
+
66
+ #: (Array[String?]) -> String
67
+ def class_list!(args)
68
+ class_list(*T.unsafe(args))
69
+ end
70
+
71
+ alias strlist class_list
72
+ alias strlist! class_list!
73
+
74
+ #: (String) -> bool
75
+ def image_exists?(path)
76
+ Boolean(h.resolve_asset_path(path))
77
+ end
78
+
79
+ end
80
+ end
81
+ end