Repository: roots/acorn (php)

Issue:
Feature Request: bundle()->dequeue
### Terms

- [X] I have read the [guidelines for Contributing to Roots Projects](https://github.com/roots/.github/blob/master/CONTRIBUTING.md)
- [X] This request is not a duplicate of an existing issue
- [X] This is not a personal support request that should be posted on the [Roots Discourse](https://discourse.roots.io/) community

### Summary

Function to dequeue loaded files (js / css)

### Motivation

#### Why are we doing this?

Some pages should not have the JS / CSS used on the rest of the site (for example a redirection page for Internet Explorer on the front, or a style applied only to some pages and not to others in the back-office)

#### What is the expected outcome?

Do not load the JS / CSS 

#### Potential conflicts / foreseeable issues

None ?


### Additional Context

_No response_

Pull request:
✨ add Bundle `dequeue()` and `when()` methods
You can now dequeue and conditionally load bundles.

```php
// directly pass boolean value
bundle('shop')->when(is_shop())->enqueue();

// pass named function to be resolved
bundle('shop')->when('is_shop')->enqueue();

// pass closure to be resolved
bundle('shop')->when(fn () => is_shop())->enqueue();
```

```php
bundle('shop')->enqueue();

// o nvm i changed my mind

bundle('shop')->dequeue();
```

closes #196

Patch:
diff --git a/src/Roots/Acorn/Assets/Bundle.php b/src/Roots/Acorn/Assets/Bundle.php
index a0a0cbe..29bf00e 100644
--- a/src/Roots/Acorn/Assets/Bundle.php
+++ b/src/Roots/Acorn/Assets/Bundle.php
@@ -4,12 +4,14 @@ namespace Roots\Acorn\Assets;
 
 use Illuminate\Support\Arr;
 use Illuminate\Support\Collection;
+use Roots\Acorn\Assets\Concerns\Conditional;
 use Roots\Acorn\Assets\Concerns\Enqueuable;
 use Roots\Acorn\Assets\Contracts\Bundle as BundleContract;
 
 class Bundle implements BundleContract
 {
     use Enqueuable;
+    use Conditional;
 
     protected $id;
     protected $path;
@@ -46,7 +48,7 @@ class Bundle implements BundleContract
      */
     public function css(?callable $callable = null)
     {
-        $styles = $this->bundle['css'];
+        $styles = $this->conditional ? $this->bundle['css'] : [];
 
         if (! $callable) {
             return collect($styles);
@@ -70,7 +72,7 @@ class Bundle implements BundleContract
      */
     public function js(?callable $callable = null)
     {
-        $scripts = array_merge($this->bundle['js'], $this->bundle['mjs']);
+        $scripts = $this->conditional ? array_merge($this->bundle['js'], $this->bundle['mjs']) : [];
 
         if (! $callable) {
             return collect($scripts);
diff --git a/src/Roots/Acorn/Assets/Concerns/Conditional.php b/src/Roots/Acorn/Assets/Concerns/Conditional.php
new file mode 100644
index 0000000..0004495
--- /dev/null
+++ b/src/Roots/Acorn/Assets/Concerns/Conditional.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Roots\Acorn\Assets\Concerns;
+
+trait Conditional
+{
+    /**
+     * Conditionally load assets.
+     *
+     * @var bool
+     */
+    protected $conditional = true;
+
+    /**
+     * Set conditional loading.
+     *
+     * @param bool|callable $conditional
+     * @return $this
+     */
+    public function when($conditional, ...$args)
+    {
+        $this->conditional = is_callable($conditional)
+            ? call_user_func($conditional, $args)
+            : $conditional;
+
+        return $this;
+    }
+}
diff --git a/src/Roots/Acorn/Assets/Concerns/Enqueuable.php b/src/Roots/Acorn/Assets/Concerns/Enqueuable.php
index c691798..7f43174 100644
--- a/src/Roots/Acorn/Assets/Concerns/Enqueuable.php
+++ b/src/Roots/Acorn/Assets/Concerns/Enqueuable.php
@@ -84,6 +84,44 @@ trait Enqueuable
         return $this->enqueueCss()->enqueueJs();
     }
 
+    /**
+     * Dequeue CSS files in WordPress.
+     *
+     * @return $this
+     */
+    public function dequeueCss()
+    {
+        $this->css(function ($handle) {
+            wp_dequeue_style($handle);
+        });
+
+        return $this;
+    }
+
+    /**
+     * Dequeue JS files in WordPress.
+     *
+     * @return $this
+     */
+    public function dequeueJs()
+    {
+        $this->js(function ($handle) {
+            wp_dequeue_script($handle);
+        });
+
+        return $this;
+    }
+
+    /**
+     * Dequeue JS and CSS files in WordPress.
+     *
+     * @return $this
+     */
+    public function dequeue()
+    {
+        return $this->dequeueCss()->dequeueJs();
+    }
+
     /**
      * Inline runtime.js in WordPress.
      *


Test patch:
diff --git a/tests/Assets/BundleTest.php b/tests/Assets/BundleTest.php
index 106e54e..1a781d4 100644
--- a/tests/Assets/BundleTest.php
+++ b/tests/Assets/BundleTest.php
@@ -44,6 +44,16 @@ it('can enqueue css', function () {
     $app->enqueueCss();
 });
 
+it('can dequeue css', function () {
+    $manifest = json_decode(file_get_contents($this->fixture('bud_single_runtime/public/entrypoints.json')), JSON_OBJECT_AS_ARRAY);
+    $app = new Bundle('app', $manifest['app'], $this->fixture('bud_single_runtime'), 'https://k.jo');
+
+    $this->stub('wp_enqueue_style')->shouldBeCalled();
+    $this->stub('wp_dequeue_style', fn (...$args) => assertMatchesSnapshot($args));
+
+    $app->enqueueCss()->dequeueCss();
+});
+
 it('can silently fail to enqueue css', function () {
     $stub = $this->stub('wp_enqueue_style');
     $manifest = json_decode(file_get_contents($this->fixture('bud_single_runtime_dev/public/entrypoints.json')), JSON_OBJECT_AS_ARRAY);
@@ -64,6 +74,18 @@ it('can enqueue js', function () {
     $app->enqueueJs();
 });
 
+it('can dequeue js', function () {
+    $manifest = json_decode(file_get_contents($this->fixture('bud_single_runtime/public/entrypoints.json')), JSON_OBJECT_AS_ARRAY);
+    $app = new Bundle('app', $manifest['app'], $this->fixture('bud_single_runtime'), 'https://k.jo');
+
+    $this->stub('wp_enqueue_script')->shouldBeCalled();
+    $this->stub('wp_add_inline_script')->shouldBeCalled();
+
+    $this->stub('wp_dequeue_script', fn (...$args) => assertMatchesSnapshot($args));
+
+    $app->enqueueJs()->dequeueJs();
+});
+
 it('can inline a single runtime', function () {
     $manifest = json_decode(file_get_contents($this->fixture('bud_single_runtime/public/entrypoints.json')), JSON_OBJECT_AS_ARRAY);
     $app = new Bundle('app', $manifest['app'], $this->fixture('bud_single_runtime'), 'https://k.jo');
@@ -118,3 +140,19 @@ it('does not inline duplicate single runtimes', function () {
     $app->enqueueJs();
     $editor->enqueueJs();
 });
+
+it('can conditionally get assets', function () {
+    $manifest = json_decode(file_get_contents($this->fixture('bud_single_runtime/public/entrypoints.json')), JSON_OBJECT_AS_ARRAY);
+    $app = new Bundle('app', $manifest['app'], $this->fixture('bud_single_runtime'), 'https://k.jo');
+
+    assertMatchesJsonSnapshot($app->js()->toJson());
+
+    expect($app->when(false)->js()->toArray())->toBeEmpty();
+    expect($app->when(true)->js()->toArray())->not()->toBeEmpty();
+
+    expect($app->when(fn () => false)->js()->toArray())->toBeEmpty();
+    expect($app->when(fn () => true)->js()->toArray())->not()->toBeEmpty();
+
+    expect($app->when('min', 0, 1)->js()->toArray())->toBeEmpty();
+    expect($app->when('max', 0, 1)->js()->toArray())->not()->toBeEmpty();
+});
diff --git a/tests/__snapshots__/BundleTest__it_can_conditionally_get_assets__1.json b/tests/__snapshots__/BundleTest__it_can_conditionally_get_assets__1.json
new file mode 100644
index 0000000..b75894c
--- /dev/null
+++ b/tests/__snapshots__/BundleTest__it_can_conditionally_get_assets__1.json
@@ -0,0 +1,3 @@
+{
+    "app": "public\/app.4476c5f869ee7584ba3c.js"
+}
diff --git a/tests/__snapshots__/BundleTest__it_can_dequeue_css__1.yml b/tests/__snapshots__/BundleTest__it_can_dequeue_css__1.yml
new file mode 100644
index 0000000..65d4575
--- /dev/null
+++ b/tests/__snapshots__/BundleTest__it_can_dequeue_css__1.yml
@@ -0,0 +1,1 @@
+- app/app
diff --git a/tests/__snapshots__/BundleTest__it_can_dequeue_js__1.yml b/tests/__snapshots__/BundleTest__it_can_dequeue_js__1.yml
new file mode 100644
index 0000000..65d4575
--- /dev/null
+++ b/tests/__snapshots__/BundleTest__it_can_dequeue_js__1.yml
@@ -0,0 +1,1 @@
+- app/app