How would I build a Bootstrap React Modal using Scala Js React - react-bootstrap

How would I create a Modal component from the React Bootstrap library using the Scala.js React library?

The fact that a Modal has to change state (show = true/false) to show/hide the dialog made the solution non-trivial. I resolved this by wrapping it in a component that had a Boolean state that could be changed - and when it needs to be shown/hidden I change state with effects impure.
Another issue was that if the Modal has buttons that need to change the state their event handlers need access to this state somehow. I resolved this issue by giving users of the component access to the Backend of the component on creation.
Here is my implementation of the Modal:
class Modal(bs: BackendScope[Unit, Boolean], onHide: => Unit, children: Modal => Seq[ChildArg]) {
def render(show: Boolean) = {
val props = (new js.Object).asInstanceOf[Modal.Props]
props.show = show
props.onHide = () => {
dismiss()
onHide
}
Modal.component(props)(children(this): _*)
}
def dismiss() = {
bs.withEffectsImpure.setState(false)
}
}
object Modal {
#JSImport("react-bootstrap", "Modal")
#js.native
object RawComponent extends js.Object
#js.native
trait Props extends js.Object {
var show: Boolean = js.native
var onHide: js.Function0[Unit] = js.native
}
val component = JsComponent[Props, Children.Varargs, Null](RawComponent)
type Unmounted = Scala.Unmounted[Unit, Boolean, Modal]
def apply(onHide: => Unit = Unit)(children: Modal => Seq[ChildArg]): Unmounted = {
val component = ScalaComponent.builder[Unit]("Modal")
.initialState(true)
.backend(new Modal(_, onHide, children))
.renderBackend
.build
component()
}
}
And a Dialog object that uses it:
object Dialog {
object Response extends Enumeration {
type Response = Value
val OK, CANCEL = Value
}
import Response._
def prompt(title: String, body: String, okText: String): Future[Response] = {
// Add an element into which we render the dialog
val element = dom.document.body.appendChild(div(id := "dialog").render).asInstanceOf[Element]
// Create a promise of a return and a method to send it
val p = Promise[Response]
def respond(ret: Response) = {
// Remove the containing element and return the response
dom.document.body.removeChild(element)
p.success(ret)
}
Modal(respond(Response.CANCEL)) { modal =>
// Function to dismiss the dialog and respond
def quit(ret: Response) = {
modal.dismiss()
respond(ret)
}
// Create the components for our Modal
Seq(
ModalHeader(true,
ModalTitle(title)
),
ModalBody(body),
ModalFooter(
Button(variant = "secondary", onClick = () => { quit(Response.CANCEL) })("Cancel"),
Button(variant = "primary", onClick = () => { quit(Response.OK) })(okText)
)
)
}.renderIntoDOM(element).backend
p.future
}
}

Related

How to get data stored as subject rxjs

I am working on displaying the details of event clicked. I have stored all the events inside an array.
When the user clicks on the event then its id is passed which checks inside the array and it passes the result into service.
showDetail(id){
let obj = this.events;
let newArr = Object.values(obj);
let result = newArr.filter(function(el) {
return el["id"] == id;
});
this.articleService.sendMessage(result);
let url = `/article/${id}`;
this.router.navigate([url]);
}
service
private detailSubject = new Subject<any>();
sendMessage(formData: any) {
this.detailSubject.next({formData});
}
getMessage(): Observable<any> {
return this.detailSubject.asObservable();
}
Now in my article/id page.
I am not being able to retrieve this passed array.
I have following code
ngOnInit() {
this.articleService.getMessage().subscribe(
res => {
this.loadArticleDetail(res["formData"]);
},
error => {
console.log("Error loading data");
}
);
}
this.articleService.sendMessage(result); // <-- Subject.next()
let url = `/article/${id}`;
this.router.navigate([url]); // <-- Subject.subscribe() after Subject.next(), so value already emitted
You already added BehaviorSubject tag. So use it. Also, getMessage(): Observable<any> { doesnt do anything except returns Observable. Feels redundant:
private detailSubject = new BehaviorSubject<any>(null);
message$ = this.detailSubject.asObservable();
sendMessage(formData: any) {
this.detailSubject.next({formData});
}
And
ngOnInit() {
this.articleService.message$.subscribe(...

angular 5, subject.next(value) does not fire

I have a problem trying to pass values into my subject and subscribing to it from another component. Here is my code that is supposed to pass my value into an observable.
private pdfLink = new Subject<string>();
pdfLinkCast = this.pdfLink.asObservable();
getPdfById(id: string): void {
this.httpClient.get<any>(this.apiUrl + id + '/pdf', this.httpOptions).subscribe((pdf) => {
// this prints
console.log(pdf);
// not sure if this is working as expected
this.pdfLink.next(pdf.url);
});
}
In my component I subscribe to it on ngOnInit as follows:
ngOnInit() {
this.subscription.add(this.someService.pdfLinkCast.subscribe((pdf) => {
// these do not print for some reason
console.log('hello world');
console.log(pdf);
}));
}

Admin on rest - implementing aor-realtime

I'm having a real hard time understanding how to implement aor-realtime (trying to use it with firebase; reads only, no write).
The first place I get stuck: This library generates a saga, right? How do I connect that with a restClient/resource? I have a few custom sagas that alert me on errors, but there is a main restClient/resource backing those. Those sagas just handles some side-effects. In this case, I just don't understand what the role of the client is, and how it interacts with the generated saga (or visa-versa)
The other question is with persistence: Updates stream in and the initial set of records is not loaded in one go. Should I be calling observer.next() with each update? or cache the updated records and call next() with the entire collection to-date.
Here's my current attempt at doing the later, but I'm still lost with how to connect it to my Admin/Resource.
import realtimeSaga from 'aor-realtime';
import { client, getToken } from '../firebase';
import { union } from 'lodash'
let cachedToken
const observeRequest = path => (type, resource, params) => {
// Filtering so that only chats are updated in real time
if (resource !== 'chat') return;
let results = {}
let ids = []
return {
subscribe(observer) {
let databaseRef = client.database().ref(path).orderByChild('at')
let events = [ 'child_added', 'child_changed' ]
events.forEach(e => {
databaseRef.on(e, ({ key, val }) => {
results[key] = val()
ids = union([ key ], ids)
observer.next(ids.map(id => results[id]))
})
})
const subscription = {
unsubscribe() {
// Clean up after ourselves
databaseRef.off()
results = {}
ids = []
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
},
};
};
export default path => realtimeSaga(observeRequest(path));
How do I connect that with a restClient/resource?
Just add the created saga to the custom sagas of your Admin component.
About the restClient, if you need it in your observer, then pass it the function which return your observer as you did with path. That's actually how it's done in the readme.
Should I be calling observer.next() with each update? or cache the updated records and call next() with the entire collection to-date.
It depends on the type parameter which is one of the admin-on-rest fetch types:
CRUD_GET_LIST: you should return the entire collection, updated
CRUD_GET_ONE: you should return the resource specified in params (which should contains its id)
Here's the solution I came up with, with guidance by #gildas:
import realtimeSaga from "aor-realtime";
import { client } from "../../../clients/firebase";
import { union } from "lodash";
const observeRequest = path => {
return (type, resource, params) => {
// Filtering so that only chats are updated in real time
if (resource !== "chats") return;
let results = {}
let ids = []
const updateItem = res => {
results[res.key] = { ...res.val(), id: res.key }
ids = Object.keys(results).sort((a, b) => results[b].at - results[a].at)
}
return {
subscribe(observer) {
const { page, perPage } = params.pagination
const offset = perPage * (page - 1)
const databaseRef = client
.database()
.ref(path)
.orderByChild("at")
.limitToLast(offset + perPage)
const notify = () => observer.next({ data: ids.slice(offset, offset + perPage).map(e => results[e]), total: ids.length + 1 })
databaseRef.once('value', snapshot => {
snapshot.forEach(updateItem)
notify()
})
databaseRef.on('child_changed', res => {
updateItem(res)
notify()
})
const subscription = {
unsubscribe() {
// Clean up after ourselves
databaseRef.off();
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
}
};
}
};
export default path => realtimeSaga(observeRequest(path));

ng2: reserve Original value if validation failed

I am trying to force the user to fill in the description when they update an item. It validates, shows error message when validation fails, stops running execution and doesn't update an item.
Please see the series of screenshot below:
However, my item is still updated even if the validation fails. It seems to me that since an object is reference in the memory, it's still updated even if it doesn't run updateTodo() method from the Todoservice.
Is it because I am just hardcoding my items just for the testing? I am very new to Angular and I don't want to implement webAPIs at this point yet.
I tried to use Object.assign({}, copy) in getTodoItem(id: number) to clone and decouple my todoItem from the list but the error message showing that it's not observable.
How can I preserve the values of Objects in the list if the validation fails? In real life application, Since we retrieve the data from the database (or webapi cache) whenever index/list component is navigated, this problem shouldn't occur. Is my assumption right?
todoService.ts
import { Itodo } from './todo'
const TodoItems: Itodo[] = [
{ todoId: 11, description: 'Silencer' },
{ todoId: 12, description: 'Centaur Warrunner' },
{ todoId: 13, description: 'Lycanthrope' },
{ todoId: 14, description: 'Sniper' },
{ todoId: 15, description: 'Lone Druid' }
]
#Injectable()
export class TodoService {
getTodoItems(): Observable<Itodo[]> {
return Observable.of(TodoItems);
}
getTodoItem(id: number): Observable<Itodo> {
return this.getTodoItems()
.map((items: Itodo[]) => items.find(p => p.todoId === id));
//let copy = this.getTodoItems()
// .map((items: Itodo[]) => items.find(p => p.todoId === id));
//return Object.assign({}, copy);
}
addNewTodo(model: Itodo): number {
return TodoItems.push(model); // return new length of an array
}
updateTodo(model: Itodo) : number {
let idx = TodoItems.indexOf(TodoItems.filter(f => f.todoId == model.todoId)[0]);
return TodoItems.splice(idx, 1, model).length; // return the count of affected item
}
}
todo-edit.component.ts -- EditItem() is the main
import { Subscription } from 'rxjs/Subscription';
import { Itodo } from './todo'
import { TodoService } from './todo.service';
#Component({
moduleId: module.id,
templateUrl: "todo-edit.component.html"
})
export class TodoEditComponent implements OnInit, OnDestroy {
todoModel: Itodo;
private sub: Subscription;
Message: string;
MessageType: number;
constructor(private _todoService: TodoService,
private _route: ActivatedRoute,
private _router: Router) {
}
ngOnInit(): void {
this.sub = this._route.params.subscribe(
params => {
let id = +params['id'];
this.getItem(id);
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
getItem(id: number) {
this._todoService.getTodoItem(id).subscribe(
item => this.todoModel = item,
error => this.Message = <any>error);
}
EditItem(): void {
this.todoModel.description = this.todoModel.description.trim();
if (!this.todoModel.description) {
this.Message = "Description must not be blank.";
this.MessageType = 2;
return;
}
console.log('valid: update now.');
let result = this._todoService.updateTodo(this.todoModel);
if (result > 0) {
this.Message = "An Item has been updated";
this.MessageType = 1;
}
else {
this.Message = "Error occured! Try again.";
this.MessageType = 2;
}
}
}
Working Solution
Object.assign it's the right method to use. I was using it wrongly in the service to clone it. You need to use it in your component, not in the service.
getItem(id: number) {
//Object.assign clone and decouple todoModel from the ArrayList
this._todoService.getTodoItem(id).subscribe(
item => this.todoModel = Object.assign({}, item),
error => this.Message = <any>error);
}
Validation does not prevent updating items, it just checks actual values for validity. You should create copy of object for editing to be able to rollback changes. You can use Object.assign({}, item) or JSON.parse(JSON.stringify(...)).

only allow children of a specific type in a react component

I have a Card component and a CardGroup component, and I'd like to throw an error when CardGroup has children that aren't Card components. Is this possible, or am I trying to solve the wrong problem?
For React 0.14+ and using ES6 classes, the solution will look like:
class CardGroup extends Component {
render() {
return (
<div>{this.props.children}</div>
)
}
}
CardGroup.propTypes = {
children: function (props, propName, componentName) {
const prop = props[propName]
let error = null
React.Children.forEach(prop, function (child) {
if (child.type !== Card) {
error = new Error('`' + componentName + '` children should be of type `Card`.');
}
})
return error
}
}
You can use the displayName for each child, accessed via type:
for (child in this.props.children){
if (this.props.children[child].type.displayName != 'Card'){
console.log("Warning CardGroup has children that aren't Card components");
}
}
You can use a custom propType function to validate children, since children are just props. I also wrote an article on this, if you want more details.
var CardGroup = React.createClass({
propTypes: {
children: function (props, propName, componentName) {
var error;
var prop = props[propName];
React.Children.forEach(prop, function (child) {
if (child.type.displayName !== 'Card') {
error = new Error(
'`' + componentName + '` only accepts children of type `Card`.'
);
}
});
return error;
}
},
render: function () {
return (
<div>{this.props.children}</div>
);
}
});
For those using a TypeScript version.
You can filter/modify components like this:
this.modifiedChildren = React.Children.map(children, child => {
if (React.isValidElement(child) && (child as React.ReactElement<any>).type === Card) {
let modifiedChild = child as React.ReactElement<any>;
// Modifying here
return modifiedChild;
}
// Returning other components / string.
// Delete next line in case you dont need them.
return child;
});
Use the React.Children.forEach method to iterate over the children and use the name property to check the type:
React.Children.forEach(this.props.children, (child) => {
if (child.type.name !== Card.name) {
console.error("Only card components allowed as children.");
}
}
I recommend to use Card.name instead of 'Card' string for better maintenance and stability in respect to uglify.
See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name
One has to use "React.isValidElement(child)" along with "child.type" if one is working with Typescript in order to avoid type mismatch errors.
React.Children.forEach(props.children, (child, index) => {
if (React.isValidElement(child) && child.type !== Card) {
error = new Error(
'`' + componentName + '` only accepts children of type `Card`.'
);
}
});
You can add a prop to your Card component and then check for this prop in your CardGroup component. This is the safest way to achieve this in React.
This prop can be added as a defaultProp so it's always there.
class Card extends Component {
static defaultProps = {
isCard: true,
}
render() {
return (
<div>A Card</div>
)
}
}
class CardGroup extends Component {
render() {
for (child in this.props.children) {
if (!this.props.children[child].props.isCard){
console.error("Warning CardGroup has a child which isn't a Card component");
}
}
return (
<div>{this.props.children}</div>
)
}
}
Checking for whether the Card component is indeed a Card component by using type or displayName is not safe as it may not work during production use as indicated here: https://github.com/facebook/react/issues/6167#issuecomment-191243709
I made a custom PropType for this that I call equalTo. You can use it like this...
class MyChildComponent extends React.Component { ... }
class MyParentComponent extends React.Component {
static propTypes = {
children: PropTypes.arrayOf(PropTypes.equalTo(MyChildComponent))
}
}
Now, MyParentComponent only accepts children that are MyChildComponent. You can check for html elements like this...
PropTypes.equalTo('h1')
PropTypes.equalTo('div')
PropTypes.equalTo('img')
...
Here is the implementation...
React.PropTypes.equalTo = function (component) {
return function validate(propValue, key, componentName, location, propFullName) {
const prop = propValue[key]
if (prop.type !== component) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
};
}
You could easily extend this to accept one of many possible types. Maybe something like...
React.PropTypes.equalToOneOf = function (arrayOfAcceptedComponents) {
...
}
static propTypes = {
children : (props, propName, componentName) => {
const prop = props[propName];
return React.Children
.toArray(prop)
.find(child => child.type !== Card) && new Error(`${componentName} only accepts "<Card />" elements`);
},
}
I published the package that allows to validate the types of React elements https://www.npmjs.com/package/react-element-proptypes :
const ElementPropTypes = require('react-element-proptypes');
const Modal = ({ header, items }) => (
<div>
<div>{header}</div>
<div>{items}</div>
</div>
);
Modal.propTypes = {
header: ElementPropTypes.elementOfType(Header).isRequired,
items: React.PropTypes.arrayOf(ElementPropTypes.elementOfType(Item))
};
// render Modal
React.render(
<Modal
header={<Header title="This is modal" />}
items={[
<Item/>,
<Item/>,
<Item/>
]}
/>,
rootElement
);
To validate correct children component i combine the use of react children foreach and the Custom validation proptypes, so at the end you can have the following:
HouseComponent.propTypes = {
children: PropTypes.oneOfType([(props, propName, componentName) => {
let error = null;
const validInputs = [
'Mother',
'Girlfried',
'Friends',
'Dogs'
];
// Validate the valid inputs components allowed.
React.Children.forEach(props[propName], (child) => {
if (!validInputs.includes(child.type.name)) {
error = new Error(componentName.concat(
' children should be one of the type:'
.concat(validInputs.toString())
));
}
});
return error;
}]).isRequired
};
As you can see is having and array with the name of the correct type.
On the other hand there is also a function called componentWithName from the airbnb/prop-types library that helps to have the same result.
Here you can see more details
HouseComponent.propTypes = {
children: PropTypes.oneOfType([
componentWithName('SegmentedControl'),
componentWithName('FormText'),
componentWithName('FormTextarea'),
componentWithName('FormSelect')
]).isRequired
};
Hope this help some one :)
Considered multiple proposed approaches, but they all turned out to be either unreliable or overcomplicated to serve as a boilerplate. Settled on the following implementation.
class Card extends Component {
// ...
}
class CardGroup extends Component {
static propTypes = {
children: PropTypes.arrayOf(
(propValue, key, componentName) => (propValue[key].type !== Card)
? new Error(`${componentName} only accepts children of type ${Card.name}.`)
: null
)
}
// ...
}
Here're the key ideas:
Utilize the built-in PropTypes.arrayOf() instead of looping over children
Check the child type via propValue[key].type !== Card in a custom validator
Use variable substitution ${Card.name} to not hard-code the type name
Library react-element-proptypes implements this in ElementPropTypes.elementOfType():
import ElementPropTypes from "react-element-proptypes";
class CardGroup extends Component {
static propTypes = {
children: PropTypes.arrayOf(ElementPropTypes.elementOfType(Card))
}
// ...
}
An easy, production friendly check. At the top of your CardGroup component:
const cardType = (<Card />).type;
Then, when iterating over the children:
React.children.map(child => child.type === cardType ? child : null);
The nice thing about this check is that it will also work with library components/sub-components that don't expose the necessary classes to make an instanceof check work.
Assert the type:
props.children.forEach(child =>
console.assert(
child.type.name == "CanvasItem",
"CanvasScroll can only have CanvasItem component as children."
)
)
Related to this post, I figured out a similar problem I had. I needed to throw an error if a child was one of many icons in a Tooltip component.
// icons/index.ts
export {default as AddIcon} from './AddIcon';
export {default as SubIcon} from './SubIcon';
...
// components/Tooltip.tsx
import { Children, cloneElement, isValidElement } from 'react';
import * as AllIcons from 'common/icons';
...
const Tooltip = ({children, ...rest}) => {
Children.forEach(children, child => {
// ** Inspired from this post
const reactNodeIsOfIconType = (node, allIcons) => {
const iconTypes = Object.values(allIcons);
return iconTypes.some(type => typeof node === 'object' && node !== null && node.type === type);
};
console.assert(!reactNodeIsOfIconType(child, AllIcons),'Use some other component instead...')
})
...
return Children.map(children, child => {
if (isValidElement(child) {
return cloneElement(child, ...rest);
}
return null;
});
}

Resources