feat(GUI): rewrite svg-icon directive in react (#1464)

We change from using an Angular directive to using React through
react2angular in the SVGIcon module.
This commit is contained in:
Benedict Aas 2017-06-12 13:42:52 +01:00 committed by Juan Cruz Viotti
parent ef739fb222
commit a7d713c323
13 changed files with 192 additions and 183 deletions

View File

@ -55,7 +55,7 @@ const app = angular.module('Etcher', [
require('./modules/drive-scanner'),
// Components
require('./components/svg-icon/svg-icon'),
require('./components/svg-icon'),
require('./components/warning-modal/warning-modal'),
require('./components/safe-webview'),

View File

@ -0,0 +1,113 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
/**
* @module Etcher.Components.SVGIcon
*/
const angular = require('angular');
const react = require('react');
const propTypes = require('prop-types');
const react2angular = require('react2angular').react2angular;
const path = require('path');
const fs = require('fs');
const MODULE_NAME = 'Etcher.Components.SVGIcon';
const angularSVGIcon = angular.module(MODULE_NAME, []);
const DEFAULT_SIZE = '40px';
/**
* @summary SVG element that takes both filepaths and file contents
* @type {Object}
* @public
*/
class SVGIcon extends react.Component {
/**
* @summary Render the SVG
* @returns {react.Element}
*/
render() {
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(__dirname, this.props.path);
const contents = fs.readFileSync(imagePath, {
encoding: 'utf8'
});
const width = this.props.width || DEFAULT_SIZE;
const height = this.props.height || DEFAULT_SIZE;
return react.createElement('div', {
className: 'svg-icon',
height,
width,
style: {
width,
height
},
disabled: this.props.disabled,
dangerouslySetInnerHTML: {
__html: contents
}
});
}
/**
* @summary Cause a re-render due to changed element properties
* @param {Object} nextProps - the new properties
*/
componentWillReceiveProps(nextProps) {
// This will update the element if the properties change
this.setState(nextProps);
}
}
SVGIcon.propTypes = {
/**
* @summary SVG contents or path to the resource
*/
path: propTypes.string.isRequired,
/**
* @summary SVG image width unit
*/
width: propTypes.string,
/**
* @summary SVG image height unit
*/
height: propTypes.string,
/**
* @summary Should the element visually appear grayed out and disabled?
*/
disabled: propTypes.bool
};
angularSVGIcon.component('svgIcon', react2angular(SVGIcon));
module.exports = MODULE_NAME;

View File

@ -1,75 +0,0 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const _ = require('lodash');
const path = require('path');
const fs = require('fs');
/**
* @summary SVGIcon directive
* @function
* @public
*
* @description
* This directive provides an easy way to load SVG icons
* by embedding the SVG contents inside the element, making
* it possible to style icons with CSS.
*
* @returns {Object}
*
* @example
* <svg-icon path="path/to/icon.svg" width="40px" height="40px"></svg-icon>
*/
module.exports = () => {
return {
templateUrl: './components/svg-icon/templates/svg-icon.tpl.html',
replace: true,
restrict: 'E',
scope: {
path: '@',
width: '@',
height: '@'
},
link: (scope, element) => {
element.css('width', scope.width || '40px');
element.css('height', scope.height || '40px');
scope.$watch('path', (value) => {
// The path contains SVG contents
if (_.first(value) === '<') {
element.html(value);
} else {
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(__dirname, value);
const contents = fs.readFileSync(imagePath, {
encoding: 'utf8'
});
element.html(contents);
}
});
}
};
};

View File

@ -0,0 +1,9 @@
svg {
width: 100%;
height: 100%;
}
svg-icon {
display: inline-block;
}

View File

@ -1,28 +0,0 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
/**
* @module Etcher.Components.SVGIcon
*/
const angular = require('angular');
const MODULE_NAME = 'Etcher.Components.SVGIcon';
const SVGIcon = angular.module(MODULE_NAME, []);
SVGIcon.directive('svgIcon', require('./directives/svg-icon'));
module.exports = MODULE_NAME;

View File

@ -1 +0,0 @@
<div class="svg-icon"></div>

View File

@ -6413,6 +6413,13 @@ body {
.modal-drive-selector-modal .word-keep {
word-break: keep-all; }
svg {
width: 100%;
height: 100%; }
svg-icon {
display: inline-block; }
/*
* Copyright 2016 resin.io
*
@ -6474,7 +6481,7 @@ body {
.page-main {
margin-top: 75px; }
.svg-icon[disabled] path {
svg-icon > div[disabled] path {
fill: #787c7f; }
.page-main .step-selection-text {
@ -6693,6 +6700,8 @@ body {
position: absolute;
right: 0;
top: 50%; }
.section-footer > span[os-open-external] {
display: flex; }
.section-loader webview {
flex: 0 1;

View File

@ -32,19 +32,21 @@
<footer class="section-footer" ng-controller="StateController as state"
ng-hide="state.currentName === 'success'">
<svg-icon path="../../../assets/etcher.svg"
width="83px"
height="13px"
os-open-external="https://etcher.io?ref=etcher_footer"></svg-icon>
<span os-open-external="https://etcher.io?ref=etcher_footer">
<svg-icon path="'../assets/etcher.svg'"
width="'83px'"
height="'13px'"></svg-icon>
</span>
<span class="caption">
is <span class="caption" os-open-external="https://github.com/resin-io/etcher">an open source project</span> by
</span>
<svg-icon path="../../../assets/resin.svg"
width="79px"
height="23px"
os-open-external="https://resin.io?ref=etcher"></svg-icon>
<span os-open-external="https://resin.io?ref=etcher">
<svg-icon path="'../assets/resin.svg'"
width="'79px'"
height="'23px'"></svg-icon>
</span>
<span class="caption footer-right"
manifest-bind="version"

View File

@ -15,21 +15,25 @@
<div class="box center">
<div class="fallback-banner">
<div class="caption caption-big">Thanks for using
<svg-icon path="../../../assets/etcher.svg"
width="150px"
height="auto"
os-open-external="https://etcher.io?ref=etcher_offline_banner"></svg-icon>
<span os-open-external="https://etcher.io?ref=etcher_offline_banner">
<svg-icon path="'../assets/etcher.svg'"
width="'150px'"
height="'auto'">
</svg-icon>
</span>
</div>
<div class="caption caption-small">
made with
<svg-icon path="../../../assets/love.svg"
width="20px"
height="auto"></svg-icon>
<svg-icon path="'../assets/love.svg'"
width="'20px'"
height="'auto'"></svg-icon>
by
<svg-icon path="../../../assets/resin.svg"
width="100px"
height="auto"
os-open-external="https://resin.io?ref=etcher_success"></svg-icon>
<span os-open-external="https://resin.io?ref=etcher_success">
<svg-icon path="'../assets/resin.svg'"
width="'100px'"
height="'auto'">
</svg-icon>
</span>
</div>
<div class="section-footer">
<span class="caption footer-right"

View File

@ -18,7 +18,7 @@
margin-top: 75px;
}
.svg-icon[disabled] path {
svg-icon > div[disabled] path {
fill: $palette-theme-dark-disabled-foreground;
}

View File

@ -2,9 +2,9 @@
<div class="col-xs" ng-controller="ImageSelectionController as image">
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
<svg-icon
class="center-block"
path="{{ main.selection.getImageLogo() || '../../../assets/image.svg' }}"></svg-icon>
<div class="center-block">
<svg-icon path="main.selection.getImageLogo() || '../assets/image.svg'"></svg-icon>
</div>
<div class="space-vertical-large">
<div ng-hide="main.selection.hasImage()">
@ -41,9 +41,10 @@
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()"></div>
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()"></div>
<svg-icon class="center-block"
path="../../../assets/drive.svg"
ng-disabled="main.shouldDriveStepBeDisabled()"></svg-icon>
<div class="center-block">
<svg-icon path="'../assets/drive.svg'"
disabled="main.shouldDriveStepBeDisabled()"></svg-icon>
</div>
<div class="space-vertical-large">
<div ng-hide="main.selection.hasDrive()">
@ -78,9 +79,10 @@
<div class="col-xs" ng-controller="FlashController as flash">
<div class="box text-center">
<svg-icon class="center-block"
path="../../../assets/flash.svg"
ng-disabled="main.shouldFlashStepBeDisabled()"></svg-icon>
<div class="center-block">
<svg-icon path="'../assets/flash.svg'"
disabled="main.shouldFlashStepBeDisabled()"></svg-icon>
</div>
<div class="space-vertical-large">
<progress-button class="button-brick"

View File

@ -33,6 +33,7 @@ $link-color: #ddd;
@import "../components/modal/styles/modal";
@import "../components/progress-button/styles/progress-button";
@import "../components/drive-selector/styles/drive-selector";
@import "../components/svg-icon/styles/svg-icon";
@import "../components/tooltip-modal/styles/tooltip-modal";
@import "../components/warning-modal/styles/warning-modal";
@import "../pages/main/styles/main";
@ -83,6 +84,10 @@ body {
right: 0;
top: 50%;
}
> span[os-open-external] {
display: flex;
}
}
.section-loader {

View File

@ -10,7 +10,7 @@ require('angular-mocks');
describe('Browser: SVGIcon', function() {
beforeEach(angular.mock.module(
require('../../../lib/gui/components/svg-icon/svg-icon')
require('../../../lib/gui/components/svg-icon')
));
describe('svgIcon', function() {
@ -18,22 +18,14 @@ describe('Browser: SVGIcon', function() {
let $compile;
let $rootScope;
beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_, $templateCache) {
beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
// Workaround `Unexpected request: GET template.html. No more request expected` error.
// See http://stackoverflow.com/a/29437480/1641422
const templatePath = './components/svg-icon/templates/svg-icon.tpl.html';
const template = fs.readFileSync(path.resolve('lib', 'gui', templatePath), {
encoding: 'utf8'
});
$templateCache.put(templatePath, template);
}));
it('should inline the svg contents in the element', function() {
const icon = '../../../../../lib/gui/assets/etcher.svg';
const icon = '../../../lib/gui/assets/etcher.svg';
let iconContents = _.split(fs.readFileSync(path.join(__dirname, '../../../lib/gui/assets/etcher.svg'), {
encoding: 'utf8'
}), /\r?\n/);
@ -43,56 +35,33 @@ describe('Browser: SVGIcon', function() {
iconContents[0] = `<!--${iconContents[0].slice(1, iconContents[0].length - 1)}-->`;
iconContents = iconContents.join('\n');
const element = $compile(`<svg-icon path="${icon}">Resin.io</svg-icon>`)($rootScope);
const element = $compile(`<svg-icon path="'${icon}'">Resin.io</svg-icon>`)($rootScope);
$rootScope.$digest();
m.chai.expect(element.html()).to.equal(iconContents);
});
it('should inline raw svg contents in the element', function() {
const svg = '<svg><text>Raspbian</text></svg>';
const element = $compile(`<svg-icon path="${svg}"></svg-icon>`)($rootScope);
$rootScope.$digest();
m.chai.expect(element.html()).to.equal(svg);
});
it('should react to external updates', function() {
const scope = $rootScope.$new();
scope.name = 'Raspbian';
scope.getSvg = () => {
return `<svg><text>${scope.name}</text></svg>`;
};
const element = $compile('<svg-icon path="{{ getSvg() }}"></svg-icon>')(scope);
$rootScope.$digest();
m.chai.expect(element.html()).to.equal('<svg><text>Raspbian</text></svg>');
scope.name = 'Resin.io';
$rootScope.$digest();
m.chai.expect(element.html()).to.equal('<svg><text>Resin.io</text></svg>');
m.chai.expect(element.children().html()).to.equal(iconContents);
});
it('should default the size to 40x40 pixels', function() {
const icon = '../../../../../lib/gui/assets/etcher.svg';
const element = $compile(`<svg-icon path="${icon}">Resin.io</svg-icon>`)($rootScope);
const icon = '../../../lib/gui/assets/etcher.svg';
const element = $compile(`<svg-icon path="'${icon}'">Resin.io</svg-icon>`)($rootScope);
$rootScope.$digest();
m.chai.expect(element.css('width')).to.equal('40px');
m.chai.expect(element.css('height')).to.equal('40px');
m.chai.expect(element.children().css('width')).to.equal('40px');
m.chai.expect(element.children().css('height')).to.equal('40px');
});
it('should be able to set a custom width', function() {
const icon = '../../../../../lib/gui/assets/etcher.svg';
const element = $compile(`<svg-icon path="${icon}" width="20px">Resin.io</svg-icon>`)($rootScope);
const icon = '../../../lib/gui/assets/etcher.svg';
const element = $compile(`<svg-icon path="'${icon}'" width="'20px'">Resin.io</svg-icon>`)($rootScope);
$rootScope.$digest();
m.chai.expect(element.css('width')).to.equal('20px');
m.chai.expect(element.css('height')).to.equal('40px');
m.chai.expect(element.children().css('width')).to.equal('20px');
m.chai.expect(element.children().css('height')).to.equal('40px');
});
it('should be able to set a custom height', function() {
const icon = '../../../../../lib/gui/assets/etcher.svg';
const element = $compile(`<svg-icon path="${icon}" height="20px">Resin.io</svg-icon>`)($rootScope);
const icon = '../../../lib/gui/assets/etcher.svg';
const element = $compile(`<svg-icon path="'${icon}'" height="'20px'">Resin.io</svg-icon>`)($rootScope);
$rootScope.$digest();
m.chai.expect(element.css('width')).to.equal('40px');
m.chai.expect(element.css('height')).to.equal('20px');
m.chai.expect(element.children().css('width')).to.equal('40px');
m.chai.expect(element.children().css('height')).to.equal('20px');
});
});