GithubHelp home page GithubHelp logo

rikschennink / conditioner Goto Github PK

View Code? Open in Web Editor NEW
1.1K 34.0 50.0 4.64 MB

๐Ÿ’†๐Ÿป Frizz free, context-aware, JavaScript modules

License: MIT License

JavaScript 93.16% HTML 6.84%
conditioner javascript es6 import responsive amd module-loader mediaqueries webpack browserify

conditioner's Introduction

Conditioner

Conditioner provides a straight forward Progressive Enhancement based solution for linking JavaScript modules to DOM elements. Modules can be linked based on contextual parameters like viewport size and element visibilty making Conditioner your perfect Responsive Design companion.

License: MIT npm version

Example

Mount a component (like a Date Picker, Section Toggler or Carrousel), but only do it on wide viewports and when the user has seen it.

<h2 data-module="/ui/component.js"
    data-context="@media (min-width:30em) and was @visible"> ... </h2>

If the viewport is resized or rotated and suddenly it's smaller than 30em Conditioner will automatically unmount the component.

Demo

Features

  • Progressive Enhancement as a starting point ๐Ÿ’†๐Ÿป
  • Perfect for a Responsive Design strategy
  • Declarative way to bind logic to elements, why this is good
  • No dependencies and small footprint (~1KB gzipped)
  • Compatible with ES import(), AMD require() and webpack
  • Can easily be extended with plugins

Resources

Installation

Install with npm:

npm install conditioner-core --save

The package includes both development and product versions. Use conditioner-core.min.js for production. The ES Module version of Conditioner (conditioner-core.esm.js) is not compressed.

Using a CDN:

<script src="https://unpkg.com/conditioner-core/conditioner-core.js"></script>

Setup

Using Conditioner on the global scope:

<script src="https://unpkg.com/conditioner-core/conditioner-core.js"></script>
<script>

// mount modules!
conditioner.hydrate( document.documentElement );

</script>

Using Conditioner async with ES modules:

import('conditioner-core/conditioner-core.esm.js').then(conditioner => {
  conditioner.addPlugin({
    // converts module aliases to paths
    moduleSetName: name => `/ui/${name}.js`,

    // get the module constructor
    moduleGetConstructor: module => module.default,

    // fetch module with dynamic import
    moduleImport: name => import(name)
  });

  // mount modules!
  conditioner.hydrate(document.documentElement);
});

Using Conditioner with webpack:

import * as conditioner from 'conditioner-core/conditioner-core.esm';

conditioner.addPlugin({
  // converts module aliases to paths
  moduleSetName: name => `./ui/${name}.js`,

  // get the module constructor
  moduleGetConstructor: module => module.default,

  // override the import
  moduleImport: name => import(`${name}`)
});

// lets go!
conditioner.hydrate(document.documentElement);

Using Conditioner in AMD modules:

require(['conditioner-core.js'], function(conditioner) {
  // setup AMD require
  conditioner.addPlugin({
    // converts module aliases to paths
    moduleSetName: function(name) {
      return '/ui/' + name + '.js';
    },

    // setup AMD require
    moduleImport: function(name) {
      return new Promise(function(resolve) {
        require([name], function(module) {
          resolve(module);
        });
      });
    }
  });

  // mount modules!
  conditioner.hydrate(document.documentElement);
});

A collection of boilerplates to get you started with various project setups:

API

Public Methods

Inspired by React and Babel, Conditioner has a tiny but extensible API.

Method Description
hydrate(element) Mount modules found in the subtree of the passed element, returns an array of bound module objects.
monitor(context[, element]) Manually monitor a context. Returns a context monitor object.
addPlugin(plugin) Add a plugin to Conditioner to extend its core functionality.

Bound Module

Bound modules are returned by the hydrate method. Each bound module object wraps a module. It exposes a set of properties, methods and callbacks to interact with the module and its element.

Property / Method Description
alias Name found in dataset.module.
name Module path after name has been passed through moduleSetName.
element The element the module is bound to.
mounted Boolean indicating wether the module is currently mounted.
mount() Manually mount the module.
unmount() Manually unmount the module.
destroy() Unmounts the module and then removes any monitors.
onmount(boundModule) Callback that runs when the module has been mounted. Scoped to element.
onmounterror(error, boundModule) Callback that runs when an error occurs during the mount process. Scoped to element.
onunmount(boundModule) Callback that runs when the module has been unmounted. Scoped to element.
ondestroy(boundModule) Callback that runs when the module has been destroyed. Scoped to element.

Context Monitor

Context Monitors are returned by the monitor method.

Property / Method Description
matches Boolean indicating wether the context is currently matches
active Boolean indicating wether it's actively monitoring the context
start() Start monitoring the context
stop() Stop monitoring the context
destroy() Stops monitoring the context and cleans up the monitor array
onchange(matches) Callback that runs when one of the monitors in the context query reported a change

An example setup is shown below.

const viewportMonitor = conditioner.monitor('@media (min-width:30em) and was @visible');

viewportMonitor.onchange = matches => {
  console.log('context is matched', matches);
};

viewportMonitor.start();

Plugins

Adding a plugin can be done with the addPlugin method, the method expects a plugin definition object.

Plugins can be used to override internal methods or add custom monitors to Conditioner.

We can link our plugins to the following hooks:

Hook Description
moduleSelector(context) Selects all elements with modules within the given context and returns a NodeList.
moduleGetContext(element) Returns context requirements for the module. By default returns the element.dataset.context attribute.
moduleImport(name) Imports the module with the given name, should return a Promise. By default searches global scope for module name.
moduleGetConstructor(module) Gets the constructor method from the module object, by default expects module parameter itself to be a factory function.
moduleGetDestructor(moduleExports) Gets the destructor method from the module constructor return value, by default expects a single function.
moduleSetConstructorArguments(name, element) Use to alter the arguments supplied to the module constructor, expects an array as return value.
moduleGetName(element) Called to get the name of the module, by default retrieves the element.dataset.module value.
moduleSetName(name) Called when the module name has been retrieved just before setting it.
moduleWillMount(boundModule) Called before the module is mounted.
moduleDidMount(boundModule) Called after the module is mounted.
moduleWillUnmount(boundModule) Called before the module is unmounted.
moduleDidUnmount(boundModule) Called after the module is unmounted.
moduleWillDestroy(boundModule) Called before the module is destroyed.
moduleDidDestroy(boundModule) Called after the module is destroyed.
moduleDidCatch(error, boundModule) Called when module import throws an error.
monitor A collection of registered monitors. See monitor setup instructions below.

File Extension Plugin Example

Instead of referencing each module with file extension (datepicker.js) we want to leave out the extension and add it automatically.

We'll use the moduleSetName hook to achieve this:

conditioner.addPlugin({
  moduleSetName: name => `${name}.js`
});

Element Visibility Monitor

Let's add a visible monitor using the IntersectionObserver API. All our custom monitors can be used in context queries by prefixing the name with an @.

Monitor plugins should mimic the MediaQueryList API. Each monitor should at least expose a matches property and an addListener method. To allow for the monitor to be unloaded we should also add the removeListener method, but it's optional.

conditioner.addPlugin({
  // the plugin "monitor" hook
  monitor: {
    // the name of our monitor, not prefixed with "@"
    name: 'visible',

    // the monitor factory method, this will create our monitor
    create: (context, element) => ({
      // current match state
      matches: false,

      // called by conditioner to start listening for changes
      addListener(change) {
        new IntersectionObserver(entries => {
          // update the matches state
          this.matches = entries.pop().isIntersecting == context;

          // inform conditioner of the new state
          change();
        }).observe(element);
      }
    })
  }
});

With our visible monitor registered, we are ready to use it in a context query.

<div data-module="/ui/component.js" data-context="@visible true"></div>

To make context queries easier to read Conditioner will automatically set the context value to true if it's omitted. So the following context query is exactle the same as @visible true.

<div data-module="/ui/component.js" data-context="@visible"></div>

To invert the monitor state we can use the not operator. Instead of writing @visible false we can now write not @visible, which again makes queries easier to read.

The @visible state context monitor will unload modules when they are no longer visible. This might be exactly what we want, for instance when animating element in and out of view. It's however more likely we want the modules to stick around after they've been loaded for the first time.

You can achieve this by adding the was statement.

<div data-module="/ui/component.js" data-context="was @visible"></div>

Now the module will stay mounted after its context has been matched for the first time.

Let's string multiple monitors together with the and operator so we can do more precise context queries.

<div data-module="/ui/component.js" data-context="@media (min-width:30em) and was @visible"></div>

Last but not least we can use the or to make exceptions. On small viewports we only mount the module when the element was visible, on big viewports we always mount the module.

<div data-module="/ui/component.js" data-context="@media (max-width:30em) and was @visible or @media(min-width:30em)"></div>

Polyfilling

To use Conditioner on older browsers you'll have to polyfill some modern JavaScript features. You could also opt to use a Mustard Cut and prevent your JavaScript from running on older browsers by wrapping the hydrate method in a feature detection statement.

// this will only run on IE10 and up
// https://caniuse.com/#feat=pagevisibility
if ('visibilityState' in document) {
  conditioner.hydrate();
}

Polyfills required for Internet Explorer 11

Internet Explorer 10

You can either polyfill dataset or add the following plugin to override the moduleGetName and moduleGetContext hooks (which use dataset).

conditioner.addPlugin({
  moduleGetName: function(element) {
    return element.getAttribute('data-module');
  },
  moduleGetContext: function(element) {
    return element.getAttribute('data-context');
  }
});

The above plugin will also be required when you need to mount modules on SVG elements. Browser support for use of dataset on SVG elements is a lot worse than HTML elements.

License

MIT

conditioner's People

Contributors

branneman avatar rikschennink avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

conditioner's Issues

Where to put helper modules

Observer, mergeObjects and matchesSelector are helper modules, they are used by the Conditioner but could also be really useful outside it. Should they be merged in the package or supplied seperately.

Add 'is active/visible' condition

add a condition which tests if the node is currently active and possibly visible in the DOM tree. This is useful for preservering resources but also to prevent problems when a module needs to measure it's containers size (which will be zero if it's not displayed).

Use MutationObservers on modern browsers (Safari, Chrome, IE11, etc.).

Maybe create a timer to test for node dimensions each x amount of milliseconds on older browsers?

Look into 'onreadystatechange' and 'propertychange' for older versions IE.

Would be interesting for modules that need to measure their container but also could be contained in tab controls and therefor might have no layout on measuring.

Optimize Test 'arrange' method.

Arrange could be more efficient if was called once per condition and not once per instance. Currently for each test related to window or element dimensions a 'resize' listener to the window is assigned, it would be better to do this once and remember the width.

Allow for 'variables' in conditions?

data-condition="element:{$foo}"
conditioner.setOptions({
    'vars':{
        'foo':'min-width:300'
    }
});

or maybe

data=conditions="night"
conditioner.setOptions({
    'states':{
        'night':'light:{low}'
    }
});

Don't require modules to return function

Use "new" constructor on Modules that return a function.

When an object is returned this object should have a "load" method which returns another object, this object should have an unload method and can be the same object as the object having the load method.

var exports = {
    load:function(element,options) {
         // do stuff
         return exports;
    },
    unload:function() {
         // clean up
    }
};
return exports;

Add 'global' module condition setup

To prevent modules from loading and allowing graceful degradation to fallback modules an entry for module requirements should be added to the conditioner setOptions method. This would for instance allow setting a geolocation condition for a module using the geolocation api, testing for support in the module itself would no longer be necessary this is now done by the conditioner before loading the module.

Move away from inheriting ModuleBase

  • Test if unload method defined, else throw error or log message.
  • set data-initialized attribute in module controller
  • what to do with options? make optional functionality? mixin?

Fix matchesSelector for IE8

p://tanalin.com/en/blog/2012/12/matches-selector-ie8/
exports.matchesSelector = function(element,selector) {
    var hits = element.parentNode.querySelectorAll(selector),
        l = hits.length,
        i=0;

    for (;i<l;i++) {
        if (hits[i] == element) {
            return true;
        }
    }
    return false;
};

How to pass options to singleton

When the BehaviorController creates an instance of a class (via, for now, the Injector) it passes default options, how do we do this for singletons.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.