-
Notifications
You must be signed in to change notification settings - Fork 96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
POC Add a trait to bootstrap ViewableData into react component #1187
base: 1
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -100,6 +100,7 @@ require('../legacy/AddToCampaignForm'); | |
require('../legacy/SecurityAdmin'); | ||
require('../legacy/ModelAdmin'); | ||
require('../legacy/ToastsContainer'); | ||
require('../legacy/BootstrapComponent'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're registering tho logic to bootstrap the react components. This would only happen once in core. |
||
|
||
// Legacy form fields | ||
// Fields used by core legacy UIs, or available to users | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import classnames from 'classnames'; | ||
|
||
const CmsSiteName = ({ title, baseHref }) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This replicates the existing SS templates but in JSX |
||
<div className="cms-sitename"> | ||
<a href="#" className="cms-sitename__link font-icon-silverstripe font-icon-large" target="_blank"> | ||
</a> | ||
<a className="cms-sitename__title" href={baseHref} target="_blank">{title}</a> | ||
</div> | ||
); | ||
|
||
CmsSiteName.propTypes = { | ||
title: PropTypes.string, | ||
baseHref: PropTypes.string, | ||
}; | ||
|
||
CmsSiteName.defaultProps = { | ||
}; | ||
|
||
export default CmsSiteName; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* global ss */ | ||
import jQuery from 'jquery'; | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import { loadComponent } from 'lib/Injector'; | ||
|
||
jQuery.entwine('ss', ($) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This bit uses entwine to wire our react component. All Component based off the BootstrapComponent trait would use this generic bootstrapping logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks good - quite similar to our implementation(below) we've got some extra handling for having an input field that the react component updates
|
||
$('.js-injector-boot .bootstrap-component').entwine({ | ||
|
||
Component: null, | ||
|
||
onmatch() { | ||
const cmsContent = this.closest('.cms-content').attr('id'); | ||
const context = (cmsContent) | ||
? { context: cmsContent } | ||
: {}; | ||
|
||
const componentName = this.data('component'); | ||
const Component = loadComponent(componentName, context); | ||
|
||
this.setComponent(Component); | ||
this._super(); | ||
this.refresh(); | ||
}, | ||
|
||
refresh() { | ||
const props = this.getProps(); | ||
const Component = this.getComponent(); | ||
ReactDOM.render(<Component {...props} />, this[0]); | ||
}, | ||
|
||
/** | ||
* Find the selected node and get attributes associated to attach the data to the form | ||
* | ||
* @returns {Object} | ||
*/ | ||
getProps() { | ||
return $(this).data('props') || {}; | ||
}, | ||
|
||
/** | ||
* Remove the component when unmatching | ||
*/ | ||
onunmatch() { | ||
ReactDOM.unmountComponentAtNode(this[0]); | ||
}, | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Admin\React; | ||
|
||
use SilverStripe\Core\Convert; | ||
|
||
trait BootstrapComponent | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This trait contains most of the logic that needs to be added to a ViewableData object to be rendered as a React component. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The trait is coupled to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd say the major advantage of keeping is a trait is that developers can add the trait to a |
||
{ | ||
|
||
private static $casting = [ | ||
'AttributesHTML' => 'HTMLFragment' | ||
]; | ||
|
||
private $attributes = []; | ||
|
||
protected $extraClasses = []; | ||
|
||
public function forTemplate() | ||
{ | ||
$return = $this->renderWith($this->getTemplates()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be nice to throw somewhere if we aren't on viewable data, just in case someone expects that to work |
||
return $return; | ||
} | ||
|
||
public function getTemplates(): array | ||
{ | ||
return [self::class, 'SilverStripe\\Admin\\React\\BootstrapComponent']; | ||
} | ||
|
||
public function getAttributesHTML($attrs = null) | ||
{ | ||
$exclude = (is_string($attrs)) ? func_get_args() : null; | ||
|
||
$attrs = $this->getAttributes(); | ||
|
||
// Remove empty | ||
$attrs = array_filter((array)$attrs, function ($value) { | ||
return ($value || $value === 0); | ||
}); | ||
|
||
// Remove excluded | ||
if ($exclude) { | ||
$attrs = array_diff_key($attrs, array_flip($exclude)); | ||
} | ||
|
||
// Prepare HTML-friendly 'method' attribute (lower-case) | ||
if (isset($attrs['method'])) { | ||
$attrs['method'] = strtolower($attrs['method']); | ||
} | ||
|
||
// Create markup | ||
$parts = []; | ||
foreach ($attrs as $name => $value) { | ||
if ($value === true) { | ||
$value = $name; | ||
} | ||
|
||
$parts[] = sprintf('%s="%s"', Convert::raw2att($name), Convert::raw2att($value)); | ||
} | ||
|
||
return implode(' ', $parts); | ||
} | ||
|
||
/** | ||
* @param string $name | ||
* @param string $value | ||
* @return $this | ||
*/ | ||
public function setAttribute($name, $value) | ||
{ | ||
$this->attributes[$name] = $value; | ||
return $this; | ||
} | ||
|
||
/** | ||
* @param string $name | ||
* @return string | ||
*/ | ||
public function getAttribute($name) | ||
{ | ||
if (isset($this->attributes[$name])) { | ||
return $this->attributes[$name]; | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* @return array | ||
*/ | ||
public function getAttributes() | ||
{ | ||
$attrs = [ | ||
'class' => $this->extraClass(), | ||
'data-component' => $this->getComponent(), | ||
'data-props' => json_encode($this->getProps()), | ||
]; | ||
|
||
$attrs = array_merge($attrs, $this->attributes); | ||
|
||
return $attrs; | ||
} | ||
|
||
/** | ||
* Compiles all CSS-classes. | ||
* | ||
* @return string | ||
*/ | ||
public function extraClass() | ||
{ | ||
return 'bootstrap-component ' . implode(' ', array_unique($this->extraClasses)); | ||
} | ||
|
||
/** | ||
* Add a CSS-class to the form-container. If needed, multiple classes can | ||
* be added by delimiting a string with spaces. | ||
* | ||
* @param string $class A string containing a classname or several class | ||
* names delimited by a single space. | ||
* @return $this | ||
*/ | ||
public function addExtraClass($class) | ||
{ | ||
//split at white space | ||
$classes = preg_split('/\s+/', $class); | ||
foreach ($classes as $class) { | ||
//add classes one by one | ||
$this->extraClasses[$class] = $class; | ||
} | ||
return $this; | ||
} | ||
|
||
/** | ||
* Remove a CSS-class from the form-container. Multiple class names can | ||
* be passed through as a space delimited string | ||
* | ||
* @param string $class | ||
* @return $this | ||
*/ | ||
public function removeExtraClass($class) | ||
{ | ||
//split at white space | ||
$classes = preg_split('/\s+/', $class); | ||
foreach ($classes as $class) { | ||
//unset one by one | ||
unset($this->extraClasses[$class]); | ||
} | ||
return $this; | ||
} | ||
|
||
abstract public function getProps(): array; | ||
|
||
abstract public function getComponent(): string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?php | ||
namespace SilverStripe\Admin\React; | ||
|
||
use SilverStripe\Admin\LeftAndMain; | ||
use SilverStripe\Control\Director; | ||
use SilverStripe\SiteConfig\SiteConfig; | ||
use SilverStripe\View\ViewableData; | ||
|
||
|
||
class SiteName extends ViewableData | ||
{ | ||
use BootstrapComponent; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks nice and clean :) |
||
|
||
public function getProps(): array | ||
{ | ||
$config = SiteConfig::current_site_config(); | ||
return [ | ||
'title' => $config->Title ?? LeftAndMain::config()->get('application_name'), | ||
'baseHref' => Director::absoluteBaseURL() | ||
]; | ||
} | ||
|
||
public function getComponent(): string | ||
{ | ||
return 'CmsSiteName'; | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<div $AttributesHTML></div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bits register our demo component in injector. Any new custom component would have to go through this step.
I nice thing here is that a developer could choose to override our component with their own custom implementation.