The Who & Elton John - Pinball Wizard (Tommy 1975)
PinballWizard
brings feature flipping into a simple and uniform API in both client-side JavaScript and Ruby.
PinballWizard is intended to work with heavily cached pages (e.g. Varnish) that need feature flipping. It works well with third parties such as Optimizely where flipping occurs after HTML is rendered.
A set of Ruby, HTML, JavaScript, and CSS that can be turned on or off.
A feature is simply a name and default state:
- active: If it is currently turned on and running.
- inactive: Not turned on.
- disabled: Not turned on and cannot be activated.
- Define and register the feature in the Ruby app.
- Build the JavaScript component.
- Write the corresponding HTML and CSS
- Activate and test your feature.
Define your feature (typically in config/initializers/pinball_wizard.rb
).
PinballWizard::DSL.build do
# Active when the page loads:
feature :example, active: true
# Deactive when the page loads:
feature :example, active: false
end
You can also pass in a proc for situtations where active/inactive is conditional. Returning false will make the feature inactive.
PinballWizard::DSL.build do
feature :example, active: proc { }
end
Once the feature is registered, you can use Slim to include the HTML partial found at app/views/features/example.slim
. This is only included if the feature is available, which allows the HTML to stay small.
= feature 'example'
To use a different partial for the same feature, pass in the partial:
key. This will use app/views/features/example_button.slim
.
If the feature is not active immediately, it's recommended to hide the HTML with inline or external CSS.
= feature 'example', partial: :example_button'
PinballWizard automatically adds and removes CSS classes named .use-{feature-name}
and without-{feature-name}
to the <html>
tag.
This supports keeping the global and feature-flipped CSS styles separated.
It is recommended to organize your CSS like so:
// app/assets/stylesheets/features/_example.scss
.use-example {
// CSS when the 'example' feature is active.
}
Then include it on the main file with @import 'features/example'
In your main SCSS file, wrap all CSS rules for the inactive state in the without-{feature-name}
class, like so:
.without-example {
// CSS when the 'example' feature is inactive;
// i.e. current production code.
}
When your feature is published (made part of the permanent codebase), you can simply delete the entire .without-example
section and remove the .use-example
class wrapper.
When using .use-{feature-name}
, you may notice a shift or flicker in the UI. This occurs when pinball's JavaScript executes after the DOMContentReady
event. To prevent this, add dist/css_tagger.min.js
into your <head>
tag. For example:
<head>
<script type="text/javascript">
// paste snippet from dist/css_tagger.min.js
</script>
</head>
be certain to update your usage of this snippet when updating pinball_wizard
Features subscribe to events and respond when they're activated or deactivated. It no longer needs to know about Optimizely, cookies, or url params. (Single Responsibility Principle FTW)
One advantage to this approach is that you can activate features after the DOM is loaded (for testing).
When pinball runs, it will automatically activate the features.
define ['pinball_wizard'], (pinball) ->
# Define feature component functions here ...
pinball.subscribe 'example',
->
# callback when activated. e.g. show it
->
# callback when deactivated. e.g. hide it.
# Return component functions to expose.
define ['pinball_wizard'], (pinball) ->
@after 'initialize', ->
pinball.subscribe 'example',
->
# callback when activated. e.g. show it
->
# callback when deactivated. e.g. hide it.
As an alternative, a feature may check if it's active. This method is not preferred since it only occurs once during page load.
define ['pinball_wizard'], (pinball) ->
if pinball.isActive('example')
# Do something
Add pinball
to the URL (e.g. ?pinball=example_a,example_b
).
pinball.activate('example');
pinball.deactivate('example');
Activating a feature that is already active or disabled will have no effect.
Add an optional source as a second argument to help know where features are activated while debugging.
pinball.activate('example','source name');
To turn on and keep a feature on, you can activate it permanently in the console. This is only for your browser's session.
pinball.activatePermanently('example')
You can permanently activate multiple features like so:
pinball.activatePermanently('example1', 'example2')
pinball.permanent()
pinball.resetPermanent()
The application keeps a list of features and passes them in the JsConfig object (e.g. window.pinball
). These define what's available and activated on page load.
<head>
<script type="text/javascript">
window.pinball = window.pinball || []
window.pinball.push(['add', { "feature_a": "active", "feature_b": "inactive", "feature_c": "disabled" }]);
</script>
<head>
head
javascript:
window.pinball = window.pinball || [];
window.pinball.push(['add', #{{PinballWizard::Registry.to_h.to_json}}]);
Add it to the url:
?pinball=debug
Turn on logging in JavaScript:
pinball.debug();
Show current state in the JavaScript console:
pinball.state();
For RentPath specific functionality, including ConFusion, see pinball_wizard-rentpath
By default, features are instances of PinballWizard::Feature
. You can define your own class and register it according to a hash key. This is useful to disable features.
# e.g. app/features/my_feature.rb
module PinballWizard
class MyFeature < Feature
def determine_state
if my_condition?
disable "My Feature: My Reason"
end
end
end
end
# e.g. config/initializers/pinball_wizard.rb
PinballWizard.configure do |c|
c.class_patterns = my_option: PinballWizard::MyFeature
end
# e.g. config/features.rb
PinballWizard::DSL.build do
feature :example, :my_option
end
git checkout dev
git pull origin dev
git checkout -b my_branch
npm run preversion
npm install
npm run watch
npm run watch:test # in another terminal window or pane
npm script commands are defined in the scripts section of package.json. To see a full list of available npm commands, run:
npm run
npm install
npm run compile
npm run watch
One time.
npm test
Watch continuously and run tests when code or specs change.
npm run watch:test
Remove compiled code and tests in .tmp/.
npm run clean
Remove compiled code and tests, node_modules
npm run clean:all
Remove compiled code, tests, node_modules
; reinstall
node modules; recompile code and tests.
npm run reset
To build a distribution and tag it, run one of the following commands.
npm version patch -m "Bumped to %s"
npm version minor -m "Bumped to %s"
npm version major -m "Bumped to %s"
There's a 'preversion' script in package.json that does the following:
- Remove the .tmp/ directory.
- Remove the node_modules directories.
- Install all npm packages.
- Compile the application source and specs.
- Run the tests.
- Rebuild the distribution.
Just build a distribution.
npm run build
- The
dist/
directory must be part of the repo - don't gitignore it!
Fork and submit a pull request. This is a README driven development process, please contribute by modifying this document.
- Pinball photo: