ft/aurelia_project/generators/redux-component.js
2017-06-09 09:09:06 +01:00

356 lines
10 KiB
JavaScript

import {inject} from 'aurelia-dependency-injection';
import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli';
import mkdirp from 'mkdirp';
@inject(Project, CLIOptions, UI)
export default class ReduxComponentGenerator {
constructor(project, options, ui) {
this.project = project;
this.options = options;
this.ui = ui;
}
execute() {
return this.ui
.ensureAnswer(this.options.args[0], 'What is the name of the redux component? (in hyphen format. ft- will be prefixed automatically)')
.then(name => {
let area = 'products';
this.ui.ensureAnswer(this.options.args[1], 'Which sub area? (blank sub area will add it at the area level)', 'portfolio')
.then(subarea => {
this.ui.ensureAnswer(this.options.args[2], 'Do you want a site specific data reducer? (Y/N)', 'Y')
.then(siteSpecificReducer => {
this.ui.ensureAnswer(this.options.args[3], 'Do you want an app state reducer? (Y/N)', 'N')
.then(appStateReducer => {
let fileName = this.project.makeFileName(name);
let className = this.project.makeClassName(name);
let funcName = this.project.makeFunctionName(name);
let makeSiteSpecificReducer = (siteSpecificReducer.toLowerCase() === 'y');
let makeAppStateReducer = (appStateReducer.toLowerCase() === 'y');
let subDir = (subarea) ? area + '/' + subarea : area;
let relDir = 'components/' + subDir + '/ft-' + fileName;
let dir = 'src/' + relDir;
mkdirp.sync(dir);// quick fix incase parent folder doesn't exist.
this.project.locations.push(this.project[dir] = ProjectItem.directory(dir));
this.project[dir].add(
ProjectItem.text(`ft-${fileName}.js`, this.generateComponentJsSource(className)),
ProjectItem.text(`ft-${fileName}.spec.js`, this.generateComponentTestJsSource(className, fileName, relDir, funcName)),
ProjectItem.text(`ft-${fileName}.html`, this.generateComponentHtmlSource(fileName)),
ProjectItem.text(`ft-${fileName}.md`, this.generateComponentMdSource('ft-' + fileName))
// ProjectItem.text(`_ft-${fileName}.scss`, this.generateComponentSassSource('ft-' + fileName, name))
);
if (makeSiteSpecificReducer) {
let reducerDir = dir + '/lib/en-us-retail';
mkdirp.sync(reducerDir);// quick fix incase parent folder doesn't exist.
this.project.locations.push(this.project[reducerDir] = ProjectItem.directory(reducerDir));
this.project[reducerDir].add(
ProjectItem.text(`${fileName}.reducer.js`, this.generateReducerJsSource(funcName, fileName)),
ProjectItem.text(`${fileName}.spec.js`, this.generateReducerTestJsSource(funcName, fileName))
);
}
if (makeAppStateReducer) {
let reducerDirAppState = dir + '/lib';
mkdirp.sync(reducerDirAppState);// quick fix incase parent folder doesn't exist.
this.project.locations.push(this.project[reducerDirAppState] = ProjectItem.directory(reducerDirAppState));
this.project[reducerDirAppState].add(
ProjectItem.text(`${fileName}.reducer.js`, this.generateAppStateReducerJsSource(funcName, fileName)),
ProjectItem.text(`${fileName}.spec.js`, this.generateAppStateReducerTestJsSource(funcName, fileName))
);
}
return this.project.commitChanges()
.then(() => this.ui.log(`Created ${fileName}.`));
});
});
});
});
}
/**
* source for top level component js
* @param className
* @returns {string}
*/
generateComponentJsSource(className) {
return `import {inject} from 'aurelia-framework';
import {ReduxComponentBase} from '../../../../lib/redux-component-base';
//import {getFundState} from '../../lib/map-state-utils';
@inject('Store', 'Beans', Element)
export class Ft${className} extends ReduxComponentBase {
populated = false;
constructor(store, beans, element) {
// add logging and integration with the data store
super(store);
this.fundId = element.getAttribute('fund-id');
// This is a top level component so we need to initiate the population
this.dispatch({
type: 'POPULATE_CHANGE_ME_STATE',
// beans: beans..., // beans set in configuration for site e.g. configuration/en-us-retail/beans.js
fundId: this.fundId
});
}
/**
* Called when the state changes.
*/
mapState(newState) {
try {
this.logger.debug('mapState()');
//let fundState = getFundState(newState, this.fundId);
// set component properties from state as appropriate
//this.label = newState.products.distributions.label;
//this.proximity = fundState.distributions.caveats.proximity;
// if we get to here without erroring, data has been populated successfully
this.populated = true;
} catch (e) {
// state not populated yet
this.populated = false;
}
}
}
`;
}
/**
* source for top level component js unit test
* @param className
* @param fileName
* @returns {string}
*/
generateComponentTestJsSource(className, fileName, relDir, funcName) {
return `import {StageComponent} from 'aurelia-testing';
import {bootstrap} from 'aurelia-bootstrapper';
import {wait} from '../../../../lib/test-utils.js';
describe('${className}', () => {
let delay = 10;
let component;
let mockStore = {
dispatch: function(action) {
expect(action.type).toBeDefined();
},
subscribe: function(callback) {
}
};
let mockBeans = { // see configuration/en-us-retail/beans.js for example
component: {
${funcName}: []
},
bean: {
}
};
let mockAttributes;
let mockState;
beforeEach(() => {
spyOn(mockStore, 'dispatch');
mockAttributes = {};
mockState = {};
component = StageComponent
.withResources('${relDir}/ft-${fileName}')
.inView(\`<ft-${fileName} id="test1"></ft-${fileName}>\`);
component.configure = (aurelia) => {
aurelia.container.registerInstance('Store', mockStore);
aurelia.container.registerInstance('Beans', mockBeans);
aurelia.use.standardConfiguration();
};
});
it('should render something', done => {
component.boundTo(mockAttributes);
component.create(bootstrap)
.then(() => {
if (component.viewModel.mapState) {
component.viewModel.mapState(mockState);
}
})
.then(wait(delay))
.then(() => {
const testElement = document.getElementById('test1');
expect(testElement.innerText.trim()).toBe('ft-${fileName}: replace me');
})
.then(done);
});
afterEach(() => {
component.dispose();
});
});
`;
}
/**
* source for top level component html
* @returns {string}
*/
generateComponentHtmlSource(fileName) {
return `<template>
<div>ft-${fileName}: replace me</div>
</template>
`;
}
/**
* source for reducer
* @param funcName, fileName
*/
generateReducerJsSource(funcName, fileName) {
return `/**
* Data Reducer for ${funcName}
* Takes site specific json data and creates model for components
* after applying business and presentation logic and data mapping
*/
import {LogManager} from 'aurelia-framework';
const logger = LogManager.getLogger('${fileName}');
export function ${funcName}(state, action) {
switch (action.type) {
case 'CHANGE_ME_TO_PROPER_ACTION_NAME_SUCCESS':
logger.debug('Reducing: CHANGE_ME_TO_PROPER_ACTION_NAME_SUCCESS');
return Object.assign({}, state, {
${funcName}: action.data
});
default:
return state;
}
}
`;
}
/**
* source for reducer unit test
* @param funcName
*/
generateReducerTestJsSource(funcName, fileName) {
return `import {${funcName}} from './${fileName}.reducer';
describe('${funcName}', () => {
it('should return unchanged state if action does not apply', done => {
let action = {
type: 'ANOTHER_ACTION'
};
let oldState = {};
let newState = ${funcName}(oldState, action, {});
expect(newState).toBe(oldState);
done();
});
it('should return some stuff', done => {
let action = {
type: 'CHANGE_ME_TO_PROPER_ACTION_NAME_SUCCESS',
data: {
stuff: 'some stuff'
}
};
let newState = ${funcName}({}, action, {});
expect(newState.${funcName}.stuff).toBe('some stuff');
done();
});
});
`;
}
/**
* source for reducer
* @param funcName, fileName
*/
generateAppStateReducerJsSource(funcName, fileName) {
return `/**
* App State Reducer for ${funcName}
* Takes application state data and creates model for components
*/
import {LogManager} from 'aurelia-framework';
const logger = LogManager.getLogger('${fileName}');
export function ${funcName}(state, action) {
switch (action.type) {
case 'SOME_ACTION':
logger.debug('Reducing: SOME_ACTION');
return Object.assign({}, state, {
${funcName}: action.data
});
default:
return state;
}
}
`;
}
/**
* source for reducer unit test
* @param funcName
*/
generateAppStateReducerTestJsSource(funcName, fileName) {
return `import {${funcName}} from './${fileName}.reducer';
describe('${funcName}', () => {
it('should return unchanged state if action does not apply', done => {
let action = {
type: 'ANOTHER_ACTION'
};
let oldState = {};
let newState = ${funcName}(oldState, action, {});
expect(newState).toBe(oldState);
done();
});
it('should return some stuff', done => {
let action = {
type: 'SOME_ACTION',
data: {
stuff: 'some stuff'
}
};
let newState = ${funcName}({}, action, {});
expect(newState.${funcName}.stuff).toBe('some stuff');
done();
});
});
`;
}
/**
* generate markdown stub
* @param fileName
* @returns {string}
*/
generateComponentMdSource(fileName) {
return `# ${ fileName }
## Usage
${'```'}html
<${ fileName} fund-id="817" cid="uniqueId"></${ fileName}>
${'```'}
*The cid is guaranteed to be unique to this page even if multiple instances of the component are added to the same page.*
## Developer notes
`;
}
/**
* generate sass partial stub
* @param fileName
* @returns {string}
*/
// generateComponentSassSource(fileName, name) {
// return `
// // CSS specific to the ${ fileName } component goes here
// [data-fti-component="${ name }"] {
// }
// `;
// }
}