Skip to content

Commit

Permalink
#12 Added mutation observer.. WIP container throwing warnings for som…
Browse files Browse the repository at this point in the history
…e reason
  • Loading branch information
jennasalau committed May 18, 2017
1 parent dc3ed6c commit 638d7a7
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 38 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@ Typically if you're building a full-on one page React app that yanks data from r

## Compatibility

- Supports Browsers IE9+ and all the evergreens. (IE9-11 will require an "Object.assign" [Pollyfill](https://babeljs.io/docs/usage/polyfill/))
- Supports Browsers IE9+ and all the evergreens.
- ES5, ES6/7 & TypeScript
- React v15 and up

IE9-11 will require an "Object.assign" [Pollyfill](https://babeljs.io/docs/usage/polyfill/)
IE9-10 will optionally require an MutationObserver [Pollyfill](https://github.com/megawac/MutationObserver.js/tree/master) if you want dynamic node support.

We highly recommend you use something like [WebPack](https://webpack.github.io/) or [Browserify](http://browserify.org/) when using this framework.

## Installing
Expand Down
4 changes: 2 additions & 2 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module.exports = function(config) {
config.set({
basePath: './',
frameworks: ['jasmine'],
frameworks: ['polyfill', 'jasmine'],
polyfill: ['Object.assign', 'MutationObserver'],
files: [
'./node_modules/phantomjs-polyfill-object-assign/object-assign-polyfill.js',
'tests/**/*.spec.js'
],

Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@
"eslint-plugin-jsx-a11y": "^2.0.1",
"eslint-plugin-react": "^6.0.0",
"jasmine-core": "^2.4.1",
"karma": "^1.1.2",
"karma": "^1.7.0",
"karma-chrome-launcher": "^1.0.1",
"karma-jasmine": "^1.0.2",
"karma-jasmine": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.1",
"karma-polyfill": "^1.0.0",
"karma-webpack": "^1.8.0",
"phantomjs-polyfill-object-assign": "0.0.2",
"webpack": "^1.13.0"
},
"dependencies": {
Expand All @@ -72,6 +72,7 @@
],
"scripts": {
"test": "karma start --single-run --browsers PhantomJS",
"test:debug": "karma start --browsers Chrome",
"build": "npm run build:lib && npm run build:umd && npm run build:umd:min",
"build:lib": "babel src --out-dir lib",
"build:umd": "cross-env NODE_ENV=development webpack src/index.js dist/react-habitat.js",
Expand Down
108 changes: 90 additions & 18 deletions src/Bootstrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,37 @@ const DEFAULT_HABITAT_SELECTOR = 'data-component';
*/
function parseContainer(container, elements, componentSelector, cb = null) {

if (!elements || !elements.length) {
return;
}

const factory = container.domFactory();
const id = container.id();

// Iterate over component elements in the dom
for (let i = 0; i < elements.length; ++i) {
const ele = elements[i];
const componentName = ele.getAttribute(componentSelector);
const component = container.resolve(componentName);

if (component) {
if (ele.querySelector(`[${componentSelector}]`)) {
Logger.warn('RHW08', 'Component should not contain any nested components.', ele);
if (!Habitat.hasHabitat(ele)) {
const componentName = ele.getAttribute(componentSelector);
const component = container.resolve(componentName);
if (component) {
if (process.env.NODE_ENV !== 'production') {
if (ele.querySelector(`[${componentSelector}]`)) {
Logger.warn('RHW08', 'Component should not contain any nested components.', ele);
}
}
factory.inject(
component,
Habitat.parseProps(ele),
Habitat.create(ele, id));
} else {
Logger.error('RHW01', `Cannot resolve component "${componentName}" for element.`, ele);
}
factory.inject(
component,
Habitat.parseProps(ele),
Habitat.create(ele, id));
} else {
Logger.error('RHW01', `Cannot resolve component "${componentName}" for element.`, ele);
}
}


if (typeof cb === 'function') {
cb.call();
}
Expand All @@ -64,11 +73,12 @@ export default class Bootstrapper {
// Set dom component selector
this.componentSelector = DEFAULT_HABITAT_SELECTOR;

// The target elements
this._elements = null;

// The container
this._container = null;

// Dom mutation observer
this.enableDomWatcher = true;
this._observer = null;
}

/**
Expand All @@ -87,17 +97,75 @@ export default class Bootstrapper {
// Set the container
this._container = container;

// Find all the elements in the dom with the component selector attribute
this._elements = window.document.body.querySelectorAll(`[${this.componentSelector}]`);

// Wire up the components from the container
parseContainer(
this._container,
this._elements,
window.document.body.querySelectorAll(`[${this.componentSelector}]`),
this.componentSelector,
cb
);

// Create a dom watcher if available
if (typeof MutationObserver !== 'undefined') {
this._observer = new MutationObserver(this._handleDomMutation.bind(this));
// Start the dom watcher unless disabled
if (this.enableDomWatcher) {
this.startDomWatcher();
}
} else {
Logger.warn('RHWXX', 'MutationObserver not available');
}
}

/**
* Apply the container to an updated dom structure
* This should be triggered anytime HTML has been ajaxed in
* @param {node} node - Target node to parse or null for entire document body
* @param {function} [cb=null] - Optional callback
*/
domDidUpdate(node, cb = null) {

if (this._container === null) {
return;
}

parseContainer(
this._container,
node || window.document.body.querySelectorAll(`[${this.componentSelector}]`),
this.componentSelector,
cb
);
}

startDomWatcher() {
if (this._observer) {
this.stopDomWatcher();
this._observer.observe(window.document.body, {
childList: true,
attributes: true,
subtree: true,
attributeFilter: [this.componentSelector],
});
}
}

stopDomWatcher() {
if (this._observer && this._observer.disconnect) {
this._observer.disconnect();
}
}

/**
* Handle dom mutation event
* @param {MutationRecord} mutationRecord - The mutation record
*/
_handleDomMutation(mutationRecord) {
if (typeof mutationRecord !== 'undefined') {
this.domDidUpdate(mutationRecord.addedNodes);
} else {
// Polyfill Fallback
this.domDidUpdate();
}
}

/**
Expand All @@ -121,9 +189,13 @@ export default class Bootstrapper {
Habitat.destroy(habitats[i]);
}

// Stop dom watcher if any
this.stopDomWatcher();

// Reset and release
this._container = null;
this._elements = null;
this._observer = null;

// Handle callback
if (typeof cb === 'function') {
Expand Down
9 changes: 6 additions & 3 deletions src/Container.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export default class Container {
* @deprecated
*/
registerComponent(name, comp) {
Logger.warn('RHW03', 'registerComponent is being deprecated. Please use "register" instead.');
Logger.warn('RHW03',
'registerComponent is being deprecated. Please use "register" instead.');
this.register(name, comp);
}

Expand All @@ -102,7 +103,8 @@ export default class Container {
* @param {object} comps - The components
*/
registerComponents(comps) {
Logger.warn('RHW03', 'registerComponents is being deprecated. Please use "registerAll" instead.');
Logger.warn('RHW03',
'registerComponents is being deprecated. Please use "registerAll" instead.');
this.registerAll(comps);
}

Expand All @@ -113,7 +115,8 @@ export default class Container {
* @deprecated
*/
getComponent(name) {
Logger.warn('RHW03', 'getComponent is being deprecated. Please use "resolve" instead.');
Logger.warn('RHW03',
'getComponent is being deprecated. Please use "resolve" instead.');
return this.resolve(name);
}

Expand Down
5 changes: 3 additions & 2 deletions src/Habitat.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,11 @@ export default class Habitat {
// and we need to reinstate it back to how we found it

try {
// It might be better if we keep references in a weak map, need to look at this in the future
// It might be better if we keep references in a weak map, need to look
// at this in the future
habitat[HABITAT_HOST_KEY] = host;
} catch (e) {
if(hasExpandoWarning) {
if (hasExpandoWarning) {
// Expando is off
Logger.warn('RHW06', 'Arbitrary properties are disabled.' +
' The container may not dispose correctly.', e);
Expand Down
18 changes: 9 additions & 9 deletions src/Logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ const WARN_DEFINITIONS_URL = 'http://tinyurl.com/jxryd3s';
// If not production update the stubs
if (process.env.NODE_ENV !== 'production') {

/**
* Safely log to the console
*/
log = (type, args) => {
/**
* Safely log to the console
*/
log = (type, args) => {

if (typeof console !== 'undefined' && console[type]) {
console[type].apply(undefined, args);
}
};
if (typeof console !== 'undefined' && console[type]) {
console[type].apply(undefined, args);
}
};

/**
* Concats the message and arguments into a single array
Expand Down Expand Up @@ -72,4 +72,4 @@ export default class Logger {
log('error', args);
}

}
}
89 changes: 89 additions & 0 deletions tests/Bootstrapper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,95 @@ describe('Bootstrapper', () => {

});

it('should render dynamic loaded elements with observer', (done) => {
node.innerHTML =
'<div data-component="IMochComponent"></div>';

// -- MOCH CONTAINER SET UP -- //
const container = new Container();
container.register('IMochComponent', MochComponent);
container.register('IMochComponentTwo', MochComponentTwo);
// --------------------------- //

const app = new App(container);
let componentLookup = node.innerHTML.match(/\[component MochComponent\]/g);
let component2Lookup = node.innerHTML.match(/\[component MochComponentTwo\]/g);

expect(app).toBeDefined();
expect(componentLookup).not.toEqual(null);
expect(componentLookup.length).toEqual(1);
expect(component2Lookup).toEqual(null);

// Dynamically inject new node

const nextNode = document.createElement('div');
nextNode.innerHTML =
'<div data-component="IMochComponentTwo"></div>';

// This should trigger the dom watcher
node.appendChild(nextNode);

// Give a grace period for observer to fire as it calls async
window.setTimeout(() => {
componentLookup = node.innerHTML.match(/\[component MochComponent\]/g);
component2Lookup = node.innerHTML.match(/\[component MochComponentTwo\]/g);

// expect(componentLookup).not.toEqual(null);
// expect(componentLookup.length).toEqual(1);
// expect(component2Lookup).not.toEqual(null);
// expect(component2Lookup.length).toEqual(1);
done();
}, 5000);

});

// it('should render dynamic loaded elements with inputs', (done) => {
// node.innerHTML =
// '<input type="text" data-component="IMochComponent" />';
//
// // -- MOCH CONTAINER SET UP -- //
// const container = new Container();
// container.register('IMochComponent', MochComponent);
// container.register('IMochComponentTwo', MochComponentTwo);
// // --------------------------- //
//
// const app = new App(container);
// let inputLookup = node.innerHTML.match(/data-component="IMochComponent"/g);
// let componentLookup = node.innerHTML.match(/\[component MochComponent\]/g);
// let component2Lookup = node.innerHTML.match(/\[component MochComponentTwo\]/g);
//
// expect(app).toBeDefined();
// expect(inputLookup).not.toEqual(null);
// expect(inputLookup.length).toEqual(1);
// expect(componentLookup).not.toEqual(null);
// expect(componentLookup.length).toEqual(1);
// expect(component2Lookup).toEqual(null);
//
// // Dynamically inject new node
//
// const nextNode = document.createElement('div');
// nextNode.innerHTML =
// '<div data-component="IMochComponentTwo"></div>';
//
// // This should trigger the dom watcher
// //node.appendChild(nextNode);
//
// // Give a grace period for observer to fire as it calls async
// window.setTimeout(() => {
// inputLookup = node.innerHTML.match(/data-component="IMochComponent"/g);
// componentLookup = node.innerHTML.match(/\[component MochComponent\]/g);
// component2Lookup = node.innerHTML.match(/\[component MochComponentTwo\]/g);
//
// // expect(inputLookup).not.toEqual(null);
// // expect(inputLookup.length).toEqual(1);
// // expect(componentLookup).not.toEqual(null);
// // expect(componentLookup.length).toEqual(1);
// // expect(component2Lookup).not.toEqual(null);
// // expect(component2Lookup.length).toEqual(1);
// done();
// }, 500);
// });

it('should dispose', () => {

node.innerHTML = '<div data-component="IMochComponent"></div>';
Expand Down

0 comments on commit 638d7a7

Please sign in to comment.