gajus / swing

A swipeable cards interface. The swipe-left/swipe-right for yes/no input. As seen in apps like Jelly and Tinder.
Other
2.63k stars 247 forks source link

Dynamically adding and removing cards from the stack #4

Open jorjordandan opened 9 years ago

jorjordandan commented 9 years ago

I've got the cards working perfectly in a prototype Meteor.js app, and I'm trying to add and remove elements from the stack. I haven't spent too much time trying, but I thought it might make a good example. Thanks again!

sachasayan commented 9 years ago

I'd appreciate this as well — haven't gone through the source yet, but I'm having trouble getting it to behave properly.

gajus commented 9 years ago

Can you describe a scenario? http://gajus.com/sandbox/swing/examples/card-state/ Example shows how to throw out card. Throw in would be close to the same.

jorjordandan commented 9 years ago

What I was trying to do was add cards to the bottom of the stack without throwing them in. I eventually added an extra targetPosition argument to the createCard function... but i never actually got it working. I can make a fork to show what I did, but not sure how helpful that will be since it didn't work.

tskweres commented 9 years ago

any updates or examples on this? What would be nice is to add cards to the bottom of the stack as cards are swiped from the top

ronobot commented 9 years ago

I've been looking into how to dynamically add new cards to the bottom of the stack, and I seem to have something working.

As far as I can tell, the issue seems to be a combination of two things: HTML display order (in the absence of declared z-indexes, nodes at the bottom of the tree appear on top of nodes earlier in the tree), and the Card object's appendToParent method, which moves any new card to the bottom of the tree (and thus the top of the visual stack).

My solution was to remove the appendToParent call from createCard, and replace it with a new method called insertBelow, which uses parent.insertBefore(element,parent.firstChild) to add the card to the top of the node list (and thus the bottom of the visual stack). In the case of my prototype, this has the added benefit of the visual stack order matching the order of the JSON data that I'm using to generate the cards.

I declared the new method directly after appendToParent:

Card.insertBelow = function (element) {
    var parent = element.parentNode;
    parent.removeChild(element);
    parent.insertBefore(element,parent.firstChild);
};

(Originally I attempted to alter appendToParent, but then I realized it was also being used for when a thrown card is picked up again. That's why I decided to make the new insertBelow method and leave appendToParent as it was. I also tried reversing the order of the array being used to create the cards, but that only ever had the illusion of effectiveness.)

Code-wise, I don't know if this is a good solution, since it adds an extra DOM manipulation everytime a card is added (whereas appendToParent seems to only act if the card is not at the bottom of the node tree, which is always the case when generating a new stack). And I haven't really tested it yet. But it's working so far, in both a plain JS prototype and an AngularJS prototype, so maybe it could be of use to people?

jorjordandan commented 9 years ago

Nice! I will have to try that out later in a meteor project I was working on.

HugoHeneault commented 8 years ago

@ronobot: do you have some working code to provide? I'm faving the same issue than you... Thanks a lot!

ronobot commented 8 years ago

@HugoHeneault The project I was working on this for got indefinitely delayed, so I wasn't able to test it further. But basically what I did was:

  1. Un-minified the swing.js file
  2. At (or around) line 1400, replaced l.appendToParent(e) with l.insertBelow(e)
  3. At (or around) line 1497, added the following function after the i.appendToParent function:
i.insertBelow = function(t) {
    var e = t.parentNode;
    e.removeChild(t);
    e.insertBefore(t,e.firstChild);
}

That's it—now the createCard method uses insertBelow instead of appendToParent.

samstarling commented 8 years ago

Has anyone ever managed to make this work? I want to be able to add a card to the bottom of the stack, and addCard adds it to the top.

adelura commented 8 years ago

I have some working prototype. Maybe it might help you (please note that, this is pseudocode):

var cards = []; // In a list
var queue = []; // In a queue
var stackMaxLength = 2;

var stack = Swing.Stack();
var ul = document.querySelector('ul.stack'); // Should be initially empty

var allCards = [
    document.createElement('ul'),
    document.createElement('ul'),
    document.createElement('ul'),
    document.createElement('ul'),
    document.createElement('ul')
];

allCards.forEach(addOrKeep);

function addOrKeep(element) {
    if (cards.length < stackMaxLength) {
        cards.push(card);

        // Adding element to DOM tree, as far as I remember to create it have to be in a DOM tree.
        ul.prepend(element);

        var card = stack.createCard(element);

        // On front
        ul.prepend(element);

        card.on('throwout', function () {
            var card = queue.shift();

            if (card) {
                stackMaxLength++;
                addOrKeep(card);
            }
        });

    } else {
        // We don't want to have it in a DOM.
        card.$element.remove();
        queue.push(card);
    }
}

If you want me to make it working example, let me know.

HugoHeneault commented 8 years ago

If you have some time to implement it, it'll be really awesome!!

2016-01-07 23:30 GMT+01:00 Artur Delura notifications@github.com:

I have some working prototype. Maybe it might help you (please note that, this is pseudocode):

var cards = []; // In a listvar queue = []; // In a queuevar stackMaxLength = 2; var stack = Swing.Stack();var ul = document.querySelector('ul.stack'); // Should be initially empty var allCards = [ document.createElement('ul'), document.createElement('ul'), document.createElement('ul'), document.createElement('ul'), document.createElement('ul') ]; allCards.forEach(addOrKeep); function addOrKeep(element) { if (cards.length < stackMaxLength) { cards.push(card);

    // Adding element to DOM tree, as far as I remember to create it have to be in a DOM tree.
    ul.prepend(element);

    var card = stack.createCard(element);

    // On front
    ul.prepend(element);

    card.on('throwout', function () {
        var card = queue.shift();

        if (card) {
            stackMaxLength++;
            addOrKeep(card);
        }
    });

} else {
    // We don't want to have it in a DOM.
    card.$element.remove();
    queue.push(card);
}

}

If you want me to make it working example, let me know.

— Reply to this email directly or view it on GitHub https://github.com/gajus/swing/issues/4#issuecomment-169827612.

miczed commented 8 years ago

I spent a couple of hours trying to add cards to the stack dynamically but I finally made it!

The problem is the following: When you call the function createCard() the function creates a new Card object and its constructor calls the function appendToParent(), which makes the Card appear on top of all other cards. If you change this function call, you can prepend cards to the stack. An even better solution would be to have two separate functions or a function parameter.

1.) Do what @ronobot suggested: Add a new function called insertBelow() and change the function call in the constructor of Card from appendToParent(..) to insertBelow(...) I uploaded my modified version to Codepen: http://codepen.io/miczed/pen/wMdevy?editors=001

2.) Write a function in JS (I used jQuery) to add a Card to the stack:

  function addNewCard(data,id) {
    var newCard = $(cardTemplate).clone();
    $(newCard).find('.content').html(data);
    $(newCard).attr('id','card_' + id);
    $('.stack').prepend($(newCard));
    stack.createCard(document.getElementById('card_' + id));
  }

I'm no JS / jQuery expert so this might not be best practice but it seems to work so far.

I have a working extended example here: http://codepen.io/miczed/pen/yebbKE

Hope this helps someone out and thanks to @gajus for the awesome plugin!

rajeevriitm commented 7 years ago

tried the first solution . I am unable to get it working . In my case I do not add cards one by one. Cards are created when i initialize it with an array from an API call. One more card is thrown out when first card is thrown out. What does e.removeChild(t); do in insertBelow function ?

This is my card.js

Card.insertBelow = function (element) {
  var parentNode = element.parentNode;
  parentNode.removeChild(element);
  parentNode.insertBefore(element, parentNode.firstChild);
};

in construct Card.insertBelow(targetElement);

Please help.

rajeevriitm commented 7 years ago

The issue was caused by pop() on array, which had to be replaced by shift()

tonysbriggs commented 5 years ago

Hey guys,

Having a similar issue...

I'm looking to add cards into the stack when a checkbox is turned on.

When a user clicks a checkbox, the associated cards are entered into the stack and users are able to swipe through them.

I can't seem to get the card stack to update when the selectedCards array is updated.

Would appreciate any help / advice.

Cheers

`export default class Cards extends Component { constructor(props, context) { super(props, context); this.config = { throwOutDistance: () => Math.max(window.innerWidth, window.innerHeight), throwOutConfidence: () => 1, allowedDirections: [ Direction.LEFT, Direction.RIGHT, ] };

const fakeEvent = {
  target: {
    id: 'digiDesign',
  }
};

this.cardRefs = new Map();

this.state = {
  selectedCategories: ['digiDesign', 'branding'],
  selectedCards: this.filterArray(fakeEvent),
  cardStack: cardData,
  currentCard: cardData.length - 1,
  cardIndex: 0,
  checked: false,
};

}

componentDidMount() { const { selectedCards } = this.state;

//Intiates key listener
document.addEventListener("keydown", this.onDirection);

//Configures Swing.js
this.stack = Stack(this.config);
this.stack.on("dragstart", () => this.setState({ dragging: true }));
this.stack.on("dragend", () => this.setState({ dragging: false }));
this.stack.on("throwout", this.onThrowOut.bind(this));

//Config stack of cards
this.configStack();

//Logs initial cards
console.log(selectedCards, 'selected cards')

}

componentWillUnmount() { this.cardRefs.forEach((ref, i) => { const el = ReactDOM.findDOMNode(this.cardRefs.get(i)); const card = this.stack.getCard(el); card.destroy(); }); }

configStack(){ const {selectedCards} = this.state; this.cardRefs.forEach((ref, i) => { const el = ReactDOM.findDOMNode(this.cardRefs.get(i)); this.stack.createCard(el, true); });

this.setState({
  currentCard: selectedCards.length - 1
})

}

//Controls keycodes onDirection = e => { if(e.keyCode === 37){ this.throwoutLeft(); } if (e.keyCode === 39) { this.throwoutRight(); } }

//Controls throwout right with key throwoutRight(){ const el = ReactDOM.findDOMNode( this.cardRefs.get(this.state.currentCard) ); const card = this.stack.getCard(el); card.throwOut(1000, 0); }

//Controls throwout left with key throwoutLeft(){ const el = ReactDOM.findDOMNode( this.cardRefs.get(this.state.currentCard) ); const card = this.stack.getCard(el); card.throwOut(-1000, 0); }

//Controls swipe/grab onThrowOut() { const { currentCard } = this.state const activeCard = this.state.currentCard

if (activeCard > 0){
  this.setState({
    currentCard: activeCard - 1
  })
}

if (activeCard === 0){
  this.setState({
    currentCard: activeCard - 1
  })
  this.resetDeck();
}

const card  = this.cardRefs.get(currentCard)
console.log(card.props, 'thrown out')

}

//Resets the deck on finish resetDeck() { const { cardStack } = this.state

this.setState({
  currentCard:cardStack.length - 1,
  resetting: true
});

this.cardRefs.forEach((ref, i) => {
  const el = ReactDOM.findDOMNode(this.cardRefs.get(i));
  const card = this.stack.getCard(el);
  card.throwIn(0, 0);
});

this.setState({
  resetting: false,
});

}

//Controls checkboxes filterArray(e){ let filteredArray = cardData.filter(function(x){ return x.id === e.target.id }); return filteredArray }

removeArray(e){ const{ selectedCards } = this.state; for (let i = selectedCards.length-1; i >= 0; i--){ if(selectedCards[i].id === e.target.id){ selectedCards.splice(i, 1) } } }

handleChange(e){ const{ selectedCards } = this.state;

if (e.target.checked){
  for (let i = 0; i < this.filterArray(e).length; i++){
    selectedCards.push(this.filterArray(e)[i])
  }
} if (!e.target.checked){
  this.removeArray(e);
}

this.setState({
  selectedCards
})

console.log(selectedCards, 'selected cards')

}

render() { const { selectedCards } = this.state; return (

{selectedCards.map((card, i) => { return ( this.cardRefs.set(i, c)} advice={card.advice} id={card.id} tag={card.tag} isSelected={this.state.selectedCategories.indexOf(card.id) > -1} active={i === this.state.currentCard} next={i === this.state.currentCard - 1} previous={i > this.state.currentCard} dragging={ (i === this.state.currentCard && this.state.dragging) || this.state.resetting } /> ) })} { filterData.map((item, i) => { const active = i === 0; return ( this.handleChange(e)} active={active} /> ) }) }
);

} }`