From 3e99fb527fcafcaac2dcc8ae73fa50d8bfb7e0d1 Mon Sep 17 00:00:00 2001 From: Jacob Herrington Date: Fri, 24 Jan 2020 15:00:24 -0600 Subject: [PATCH] Add StimulusJS and Refactor Internal article_script (#5539) deploy] * Add StimulusJS 1.1.1 Because: - Internal will likely need more interactivity going forward - Vanilla JS and jQuery are great, but aren't quite as organized as we might like - Stimulus fits our model of what internal should be (imo) - Adopting Preact or something with templating would require rewriting a bunch of erb (that is currently working just fine) Stimulus is a JavaScript framework that directly compliments turbolinks and the approach vanilla Rails takes to JavaScript. I've set it up to specifically be used in the internal views where we are currently using jQuery and vanilla JS. I think we should chip away at the vanilla JS currently in place with this, which appears to be both scalable and pragmatic exactly the sort of tool we want for building our internal tools. * Replace crufty article JS with Stimulus I'm swapping out some of the stuff going on in article_script.erb with some StimulusJS, it seems like a good test case for Stimulus. This lets us organize the code a little more intentionally and hopefully fixes a small performance complaint that Michael had. * Rename uncommunicative methods * Add unit tests for Stimulus classes * Move Stimulus code to javascript/internal --- .../internal/__mocks__/mutationObserver.js | 23 +++++++ .../controllers/article_controller.test.js | 61 +++++++++++++++++++ .../controllers/buffer_controller.test.js | 52 ++++++++++++++++ .../controllers/article_controller.js | 32 ++++++++++ .../internal/controllers/buffer_controller.js | 30 +++++++++ app/javascript/packs/internal.js | 6 ++ ...html.erb => _image_upload_script.html.erb} | 45 -------------- .../articles/_individual_article.html.erb | 19 +++--- app/views/internal/articles/index.html.erb | 20 +++--- app/views/internal/articles/show.html.erb | 1 - .../classified_listings/index.html.erb | 7 +-- app/views/layouts/internal.html.erb | 5 ++ package.json | 2 + yarn.lock | 37 +++++++++++ 14 files changed, 270 insertions(+), 70 deletions(-) create mode 100644 app/javascript/internal/__mocks__/mutationObserver.js create mode 100644 app/javascript/internal/__tests__/controllers/article_controller.test.js create mode 100644 app/javascript/internal/__tests__/controllers/buffer_controller.test.js create mode 100644 app/javascript/internal/controllers/article_controller.js create mode 100644 app/javascript/internal/controllers/buffer_controller.js create mode 100644 app/javascript/packs/internal.js rename app/views/internal/articles/{_article_script.html.erb => _image_upload_script.html.erb} (55%) diff --git a/app/javascript/internal/__mocks__/mutationObserver.js b/app/javascript/internal/__mocks__/mutationObserver.js new file mode 100644 index 000000000..bd9e91cba --- /dev/null +++ b/app/javascript/internal/__mocks__/mutationObserver.js @@ -0,0 +1,23 @@ +// Sourced from https://github.com/stimulusjs/stimulus/issues/34 +// and https://shime.sh/testing-stimulus + +// Setup MutationObserver shim since jsdom doesn't +// support it out of the box. + +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +const shim = readFileSync( + resolve( + 'node_modules', + 'mutationobserver-shim', + 'dist', + 'mutationobserver.min.js', + ), + { encoding: 'utf-8' }, +); + +const script = window.document.createElement('script'); +script.textContent = shim; + +window.document.body.appendChild(script); diff --git a/app/javascript/internal/__tests__/controllers/article_controller.test.js b/app/javascript/internal/__tests__/controllers/article_controller.test.js new file mode 100644 index 000000000..e7011c9be --- /dev/null +++ b/app/javascript/internal/__tests__/controllers/article_controller.test.js @@ -0,0 +1,61 @@ +import { Application } from 'stimulus'; +import ArticleController from '../../controllers/article_controller'; +import '../../__mocks__/mutationObserver'; + +describe('ArticleController', () => { + beforeEach(() => { + document.body.innerHTML = `
+ + + + +
`; + + const application = Application.start(); + application.register('article', ArticleController); + }); + + // Unix timestamp, one hour ago + const initialValue = Math.round((Date.now() - 3600000) / 1000); + + describe('#increaseFeaturedNumber', () => { + it('increases the featured number input', () => { + const button = document.querySelectorAll('button')[0]; + const input = document.querySelector( + "[data-target='article.featuredNumber']", + ); + + input.value = initialValue; + button.click(); + + expect(parseInt(input.value, 10)).toBeGreaterThan(initialValue); + }); + }); + + describe('#decreaseFeaturedNumber', () => { + it('increases the featured number input', () => { + const button = document.querySelectorAll('button')[1]; + const input = document.querySelector( + "[data-target='article.featuredNumber']", + ); + + input.value = initialValue; + button.click(); + + expect(parseInt(input.value, 10)).toBeLessThan(initialValue); + }); + }); + + describe('#highlightElement', () => { + it('adds a class to the controller element', () => { + const button = document.querySelectorAll('button')[2]; + const element = document.querySelector("[data-controller='article']"); + + button.click(); + + expect( + element.classList.contains('highlighted-bg', 'highlighted-border'), + ).toBe(true); + }); + }); +}); diff --git a/app/javascript/internal/__tests__/controllers/buffer_controller.test.js b/app/javascript/internal/__tests__/controllers/buffer_controller.test.js new file mode 100644 index 000000000..0fef95860 --- /dev/null +++ b/app/javascript/internal/__tests__/controllers/buffer_controller.test.js @@ -0,0 +1,52 @@ +import { Application } from 'stimulus'; +import BufferController from '../../controllers/buffer_controller'; +import '../../__mocks__/mutationObserver'; + +describe('BufferController', () => { + beforeEach(() => { + document.body.innerHTML = `
+

+ + + +
`; + + const application = Application.start(); + application.register('buffer', BufferController); + }); + + describe('#tagBufferUpdateConfirmed', () => { + it('adds a badge to the header', () => { + const button = document.querySelectorAll('button')[0]; + const header = document.querySelector('h2'); + + button.click(); + + expect(header.firstChild.textContent).toMatch(/Confirm/); + }); + }); + + describe('#tagBufferUpdateDismissed', () => { + it('adds a badge to the header', () => { + const button = document.querySelectorAll('button')[1]; + const header = document.querySelector('h2'); + + button.click(); + + expect(header.firstChild.textContent).toMatch(/Dismiss/); + }); + }); + + describe('#highlightElement', () => { + it('adds a class to the controller element', () => { + const button = document.querySelectorAll('button')[2]; + const element = document.querySelector("[data-controller='buffer']"); + + button.click(); + + expect( + element.classList.contains('highlighted-bg', 'highlighted-border'), + ).toBe(true); + }); + }); +}); diff --git a/app/javascript/internal/controllers/article_controller.js b/app/javascript/internal/controllers/article_controller.js new file mode 100644 index 000000000..9d515db86 --- /dev/null +++ b/app/javascript/internal/controllers/article_controller.js @@ -0,0 +1,32 @@ +import { Controller } from 'stimulus'; + +export default class ArticleController extends Controller { + static targets = ['featuredNumber']; + + increaseFeaturedNumber() { + // Increases the article's chances of being seen + const seconds = new Date().getTime() / 1000; + this.featuredNumberTarget.value = Math.round(seconds); + } + + decreaseFeaturedNumber() { + // Decreases the article's chances of being seen + const seconds = new Date().getTime() / 1080; + this.featuredNumberTarget.value = Math.round(seconds); + } + + highlightElement() { + this.element.classList.add('highlighted-bg', 'highlighted-border'); + setTimeout(() => { + this.element.classList.remove('highlighted-bg'); + }, 350); + } + + get articleId() { + return parseInt(this.data.get('id'), 10); + } + + set articleId(value) { + this.data.set('id', value); + } +} diff --git a/app/javascript/internal/controllers/buffer_controller.js b/app/javascript/internal/controllers/buffer_controller.js new file mode 100644 index 000000000..47b7c9323 --- /dev/null +++ b/app/javascript/internal/controllers/buffer_controller.js @@ -0,0 +1,30 @@ +import { Controller } from 'stimulus'; + +export default class BufferController extends Controller { + static targets = ['header']; + + tagBufferUpdateConfirmed() { + this.headerTarget.innerHTML += + 'Confirm'; + } + + tagBufferUpdateDismissed() { + this.headerTarget.innerHTML += + 'Dismiss'; + } + + highlightElement() { + this.element.classList.add('highlighted-bg', 'highlighted-border'); + setTimeout(() => { + this.element.classList.remove('highlighted-bg'); + }, 350); + } + + get bufferUpdateId() { + return parseInt(this.data.get('id'), 10); + } + + set bufferUpdateId(value) { + this.data.set('id', value); + } +} diff --git a/app/javascript/packs/internal.js b/app/javascript/packs/internal.js new file mode 100644 index 000000000..e149f445e --- /dev/null +++ b/app/javascript/packs/internal.js @@ -0,0 +1,6 @@ +import { Application } from 'stimulus'; +import { definitionsFromContext } from 'stimulus/webpack-helpers'; + +const application = Application.start(); +const context = require.context('internal/controllers', true, /.js$/); +application.load(definitionsFromContext(context)); diff --git a/app/views/internal/articles/_article_script.html.erb b/app/views/internal/articles/_image_upload_script.html.erb similarity index 55% rename from app/views/internal/articles/_article_script.html.erb rename to app/views/internal/articles/_image_upload_script.html.erb index 76ea1f3bc..09e215312 100644 --- a/app/views/internal/articles/_article_script.html.erb +++ b/app/views/internal/articles/_image_upload_script.html.erb @@ -1,50 +1,5 @@ diff --git a/package.json b/package.json index b94c36f10..8c1bd0640 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "jest-fetch-mock": "^3.0.1", "jsdom": "^15.2.1", "lint-staged": "^10.0.0", + "mutationobserver-shim": "^0.3.3", "preact-render-spy": "1.3.0", "preact-render-to-json": "^3.6.6", "prettier": "^1.19.1", @@ -105,6 +106,7 @@ "preact-textarea-autosize": "^4.0.7", "prop-types": "^15.7.2", "pusher-js": "^5.0.3", + "stimulus": "^1.1.1", "twilio-video": "^2.0.0", "web-share-wrapper": "^0.2.1" }, diff --git a/yarn.lock b/yarn.lock index ecabd2193..835a6a456 100644 --- a/yarn.lock +++ b/yarn.lock @@ -290,6 +290,30 @@ dependencies: any-observable "^0.3.0" +"@stimulus/core@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@stimulus/core/-/core-1.1.1.tgz#42b0cfe5b73ca492f41de64b77a03980bae92c82" + integrity sha512-PVJv7IpuQx0MVPCBblXc6O2zbCmU8dlxXNH4bC9KK6LsvGaE+PCXXrXQfXUwAsse1/CmRu/iQG7Ov58himjiGg== + dependencies: + "@stimulus/mutation-observers" "^1.1.1" + +"@stimulus/multimap@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@stimulus/multimap/-/multimap-1.1.1.tgz#b95e3fd607345ab36e5d5b55486ee1a12d56b331" + integrity sha512-26R1fI3a8uUj0WlMmta4qcfIQGlagegdP4PTz6lz852q/dXlG6r+uPS/bx+H8GtfyS+OOXVr3SkZ0Zg0iRqRfQ== + +"@stimulus/mutation-observers@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@stimulus/mutation-observers/-/mutation-observers-1.1.1.tgz#0f6c6f081308427fed2a26360dda0c173b79cfc0" + integrity sha512-/zCnnw1KJlWO2mrx0yxYaRFZWMGnDMdOgSnI4hxDLxdWVuL2HMROU8FpHWVBLjKY3T9A+lGkcrmPGDHF3pfS9w== + dependencies: + "@stimulus/multimap" "^1.1.1" + +"@stimulus/webpack-helpers@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@stimulus/webpack-helpers/-/webpack-helpers-1.1.1.tgz#eff60cd4e58b921d1a2764dc5215f5141510f2c2" + integrity sha512-XOkqSw53N9072FLHvpLM25PIwy+ndkSSbnTtjKuyzsv8K5yfkFB2rv68jU1pzqYa9FZLcvZWP4yazC0V38dx9A== + "@storybook/addon-actions@3.4.12": version "3.4.12" resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-3.4.12.tgz#ff6cbaf563c3cb5d648d6a35f66cfa50ced49bf4" @@ -8297,6 +8321,11 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" +mutationobserver-shim@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#65869630bc89d7bf8c9cd9cb82188cd955aacd2b" + integrity sha512-gciOLNN8Vsf7YzcqRjKzlAJ6y7e+B86u7i3KXes0xfxx/nfLmozlW1Vn+Sc9x3tPIePFgc1AeIFhtRgkqTjzDQ== + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -12071,6 +12100,14 @@ stickyfill@^1.1.1: resolved "https://registry.yarnpkg.com/stickyfill/-/stickyfill-1.1.1.tgz#39413fee9d025c74a7e59ceecb23784cc0f17f02" integrity sha1-OUE/7p0CXHSn5ZzuyyN4TMDxfwI= +stimulus@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stimulus/-/stimulus-1.1.1.tgz#53c2fded6849e7b85eed3ed8dd76e33abd74bec5" + integrity sha512-R0mBqKp48YnRDZOxZ8hiOH4Ilph3Yj78CIFTBkCwyHs4iGCpe7xlEdQ7cjIxb+7qVCSxFKgxO+mAQbsNgt/5XQ== + dependencies: + "@stimulus/core" "^1.1.1" + "@stimulus/webpack-helpers" "^1.1.1" + store2@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/store2/-/store2-2.7.1.tgz#22070b7dc04748a792fc6912a58ab99d3a21d788"